Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
907bf0e
Auto-detect HF model cache to bypass Rust TLS in restricted environments
claude Jun 17, 2026
c1ca1bb
Merge pull request #1 from danielhertz1999-bit/claude/awesome-turing-…
danielhertz1999-bit Jun 17, 2026
1dc1d64
Add platform-agnostic IPC transport layer for Windows porting
danielhertz1999-bit Jun 18, 2026
8154b9b
Step 2: Replace fcntl file-locking with platform-agnostic shim
danielhertz1999-bit Jun 18, 2026
c009736
Steps 3+4+9: Guard resource, POSIX signals, and log paths for Windows
danielhertz1999-bit Jun 18, 2026
8ecd257
Steps 7+10: Guard os.geteuid, os.fchmod, and add icacls key security
danielhertz1999-bit Jun 18, 2026
0e8321c
Step 5: Windows Task Scheduler daemon installer + handoff doc
danielhertz1999-bit Jun 18, 2026
f4865bf
Step 6: Windows PowerShell hook equivalents + hook installer updates
danielhertz1999-bit Jun 18, 2026
0d41e0a
Update WINDOWS_PORT_HANDOFF.md: mark Steps 1-6 complete, add next ste…
danielhertz1999-bit Jun 18, 2026
59839a3
Step 7: Port bench RSS helpers off POSIX resource module
danielhertz1999-bit Jun 18, 2026
13808e1
Fix lifecycle_event_log import regression from Step 2
danielhertz1999-bit Jun 19, 2026
019e52f
Make `daemon` subcommand help text Windows-aware
danielhertz1999-bit Jun 19, 2026
1b2ca70
Update WINDOWS_PORT_HANDOFF.md with in-situ Windows verification results
danielhertz1999-bit Jun 19, 2026
269e90a
Fix test collection on Windows: swap fcntl/resource for cross-platfor…
danielhertz1999-bit Jun 20, 2026
5b77bd9
Fix flock() offset mutation and Claude Desktop wrapper guard on Windows
claude Jun 20, 2026
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
407 changes: 407 additions & 0 deletions WINDOWS_PORT_HANDOFF.md

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion bench/consolidation_rss_peak.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import gc
import json
import os
import resource
import shutil
import sys
import tempfile
Expand Down Expand Up @@ -38,6 +37,14 @@ def _cur_rss_bytes() -> int:


def _ru_maxrss_bytes() -> int:
if sys.platform == "win32":
try:
import psutil
mi = psutil.Process().memory_info()
return int(getattr(mi, "peak_wset", mi.rss))
except Exception:
return 0
import resource
r = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
if sys.platform == "darwin":
return int(r)
Expand Down
49 changes: 38 additions & 11 deletions bench/embed_warm_cost.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,42 @@
"""

_PAYLOAD_RSS = r"""
import sys, resource
import sys
sys.path.insert(0, {src_path!r})
import platform as _plat
_system = _plat.system()
if _system == "Windows":
import psutil as _psutil
def _peak_raw():
mi = _psutil.Process().memory_info()
return int(getattr(mi, "peak_wset", mi.rss))
def _to_mb(raw):
return raw / 1048576
_unit_is_bytes = True
else:
import resource as _resource
def _peak_raw():
return _resource.getrusage(_resource.RUSAGE_SELF).ru_maxrss
if _system == "Darwin":
def _to_mb(raw):
return raw / 1048576
_unit_is_bytes = True
else:
def _to_mb(raw):
return raw / 1024
_unit_is_bytes = False
from iai_mcp.embed import Embedder
e = Embedder()
rss_post_construct_raw = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
rss_post_construct_raw = _peak_raw()
text = {text!r}
_ = e.embed(text)
rss_post_encode_raw = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
import platform as _plat
is_mac = (_plat.system() == "Darwin")
def to_mb(raw):
return raw / 1048576 if is_mac else raw / 1024
print(f"rss_post_construct_mb={{to_mb(rss_post_construct_raw):.1f}}")
print(f"rss_post_encode_mb={{to_mb(rss_post_encode_raw):.1f}}")
rss_post_encode_raw = _peak_raw()
print(f"rss_post_construct_mb={{_to_mb(rss_post_construct_raw):.1f}}")
print(f"rss_post_encode_mb={{_to_mb(rss_post_encode_raw):.1f}}")
print(f"rss_post_construct_raw={{rss_post_construct_raw}}")
print(f"rss_post_encode_raw={{rss_post_encode_raw}}")
print(f"unit_is_bytes={{is_mac}}")
print(f"unit_is_bytes={{_unit_is_bytes}}")
print(f"rss_platform={{_system}}")
"""


Expand Down Expand Up @@ -210,17 +229,25 @@ def measure_rss(src_path: str, text: str) -> dict:
rss_post_construct_mb = float(kv["rss_post_construct_mb"])
rss_post_encode_mb = float(kv["rss_post_encode_mb"])
unit_is_bytes = kv["unit_is_bytes"] == "True"
rss_platform = kv.get("rss_platform", "")
if rss_platform == "Windows":
unit_label = "bytes (Windows peak_wset)"
elif rss_platform == "Darwin" or (unit_is_bytes and not rss_platform):
unit_label = "bytes (macOS)"
else:
unit_label = "KB (Linux)"
print(
f" RSS post-construct={rss_post_construct_mb:.1f}MB "
f"post-first-encode={rss_post_encode_mb:.1f}MB "
f"unit={'bytes (macOS)' if unit_is_bytes else 'KB (Linux)'}"
f"unit={unit_label}"
)
return {
"rss_post_construct_mb": rss_post_construct_mb,
"rss_post_encode_mb": rss_post_encode_mb,
"rss_post_construct_raw": int(kv["rss_post_construct_raw"]),
"rss_post_encode_raw": int(kv["rss_post_encode_raw"]),
"unit_is_bytes_macos": unit_is_bytes,
"rss_platform": rss_platform,
}


Expand Down
6 changes: 5 additions & 1 deletion bench/memory_footprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import gc
import json
import os
import resource
import sys
import tempfile
import time
Expand Down Expand Up @@ -42,6 +41,11 @@ def _threshold_mb_for_n(n: int) -> float:


def _rss_mb() -> float:
if sys.platform == "win32":
import psutil
mi = psutil.Process().memory_info()
return float(getattr(mi, "peak_wset", mi.rss)) / 1024.0 / 1024.0
import resource
r = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
if sys.platform == "darwin":
return float(r) / 1024.0 / 1024.0
Expand Down
6 changes: 5 additions & 1 deletion bench/memorygraph_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import argparse
import gc
import resource
import sys
from pathlib import Path
from uuid import uuid4
Expand All @@ -14,6 +13,11 @@


def rss_mb() -> float:
if sys.platform == "win32":
import psutil
mi = psutil.Process().memory_info()
return float(getattr(mi, "peak_wset", mi.rss)) / (1024 * 1024)
import resource
ru = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
if sys.platform == "darwin":
return ru / (1024 * 1024)
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ iai_mcp = [
"_deploy/launchd/*.plist",
"_deploy/systemd/*.service",
"_deploy/hooks/*.sh",
"_deploy/hooks/*.ps1",
"_wrapper/*.js",
]

Expand Down
146 changes: 146 additions & 0 deletions src/iai_mcp/_deploy/hooks/iai-mcp-session-capture.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# IAI-MCP Stop hook — ambient WRITE-side capture (Windows).
#
# PowerShell equivalent of iai-mcp-session-capture.sh.
# Fires when a Claude Code session ends. Calls `iai-mcp capture-transcript
# --no-spawn` to batch-capture the session transcript.
# Fail-safe: always exits 0.

$ErrorActionPreference = 'SilentlyContinue'

try {
$inputText = [Console]::In.ReadToEnd()
} catch {
$inputText = ''
}

$session_id = ''
$transcript_path = ''
$cwd = ''
try {
$obj = $inputText | ConvertFrom-Json
$session_id = if ($obj.session_id) { $obj.session_id } else { '' }
$transcript_path = if ($obj.transcript_path) { $obj.transcript_path } else { '' }
$cwd = if ($obj.cwd) { $obj.cwd } else { '' }
} catch {}

# Fallback: locate transcript if the hook payload didn't include its path.
if (-not $transcript_path -and $session_id) {
$projectsDir = Join-Path $env:USERPROFILE '.claude\projects'
if (Test-Path $projectsDir) {
Get-ChildItem -Path $projectsDir -Directory | ForEach-Object {
$candidate = Join-Path $_.FullName "$session_id.jsonl"
if ((Test-Path $candidate) -and -not $transcript_path) {
$transcript_path = $candidate
}
}
}
}

$logDir = Join-Path $env:USERPROFILE '.iai-mcp\logs'
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
$logDate = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd')
$logFile = Join-Path $logDir "capture-$logDate.log"
$ts = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')

Add-Content -Path $logFile -Value "---" -ErrorAction SilentlyContinue
Add-Content -Path $logFile -Value "$ts session=$session_id cwd=$cwd transcript=$transcript_path" -ErrorAction SilentlyContinue

if (-not $transcript_path -or -not (Test-Path $transcript_path)) {
Add-Content -Path $logFile -Value "$ts skipped: no transcript found" -ErrorAction SilentlyContinue
exit 0
}

# Rename the active-writer marker so the drain can see it.
if ($session_id) {
$liveFile = Join-Path $env:USERPROFILE ".iai-mcp\.deferred-captures\$session_id.live.jsonl"
if (Test-Path $liveFile) {
$epoch = [int][double]::Parse((Get-Date -UFormat '%s'))
$newName = "$session_id.live-$epoch.jsonl"
$destDir = Split-Path $liveFile -Parent
Move-Item -Path $liveFile -Destination (Join-Path $destDir $newName) -Force -ErrorAction SilentlyContinue
}
$offsetState = Join-Path $env:USERPROFILE ".iai-mcp\.capture-state\$session_id.offset"
if (Test-Path $offsetState) { Remove-Item -Path $offsetState -Force -ErrorAction SilentlyContinue }
}

# Find the iai-mcp CLI
$iai_cli = $null

# 1. Environment variable override
if ($env:IAI_MCP_SESSION_CAPTURE_CLI -and (Test-Path $env:IAI_MCP_SESSION_CAPTURE_CLI)) {
$iai_cli = $env:IAI_MCP_SESSION_CAPTURE_CLI
}

# 2. Cached CLI path
if (-not $iai_cli) {
$cliCache = Join-Path $env:USERPROFILE '.iai-mcp\.cli-path'
if (Test-Path $cliCache) {
$cached = (Get-Content $cliCache -ErrorAction SilentlyContinue).Trim()
if ($cached -and (Test-Path $cached)) { $iai_cli = $cached }
}
}

# 3. PATH lookup
if (-not $iai_cli) {
try {
$resolved = (Get-Command iai-mcp -ErrorAction Stop).Source
if ($resolved) {
$iai_cli = $resolved
Set-Content -Path (Join-Path $env:USERPROFILE '.iai-mcp\.cli-path') -Value $iai_cli -ErrorAction SilentlyContinue
}
} catch {}
}

# 4. Common Windows install locations
if (-not $iai_cli) {
$candidates = @(
(Join-Path $env:USERPROFILE '.local\bin\iai-mcp.exe'),
(Join-Path $env:USERPROFILE 'IAI-MCP\.venv\Scripts\iai-mcp.exe'),
(Join-Path $env:LOCALAPPDATA 'Programs\Python\Scripts\iai-mcp.exe')
)
foreach ($c in $candidates) {
if (Test-Path $c) {
$iai_cli = $c
Set-Content -Path (Join-Path $env:USERPROFILE '.iai-mcp\.cli-path') -Value $iai_cli -ErrorAction SilentlyContinue
break
}
}
}

# 5. Fall back to python -m iai_mcp
if (-not $iai_cli) {
$pyExe = $null
try { $pyExe = (Get-Command python -ErrorAction Stop).Source } catch {}
if ($pyExe) {
$iai_cli = "__python__"
}
}

if (-not $iai_cli) {
Add-Content -Path $logFile -Value "$ts skipped: iai-mcp CLI not found" -ErrorAction SilentlyContinue
exit 0
}

# Run capture with a 30s timeout
try {
if ($iai_cli -eq "__python__") {
$pyExe = (Get-Command python -ErrorAction Stop).Source
$proc = Start-Process -FilePath $pyExe `
-ArgumentList '-m', 'iai_mcp', 'capture-transcript', '--no-spawn', '--session-id', $session_id, '--max-turns', '100000', $transcript_path `
-NoNewWindow -PassThru -RedirectStandardOutput (Join-Path $logDir 'capture-stdout.tmp') -RedirectStandardError (Join-Path $logDir 'capture-stderr.tmp')
} else {
$proc = Start-Process -FilePath $iai_cli `
-ArgumentList 'capture-transcript', '--no-spawn', '--session-id', $session_id, '--max-turns', '100000', $transcript_path `
-NoNewWindow -PassThru -RedirectStandardOutput (Join-Path $logDir 'capture-stdout.tmp') -RedirectStandardError (Join-Path $logDir 'capture-stderr.tmp')
}
$exited = $proc.WaitForExit(30000)
if (-not $exited) {
try { $proc.Kill() } catch {}
}
$rc = if ($exited) { $proc.ExitCode } else { 124 }
} catch {
$rc = 1
}

Add-Content -Path $logFile -Value "$ts rc=$rc" -ErrorAction SilentlyContinue
exit 0
Loading