feat(qmd): warm the index and daemon from a SessionStart hook
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
08f015de92
commit
f3c88a089e
12 files changed
+202
-14
No files matched your search
@@ -16,7 +16,10 @@
|
|||||||
},
|
},
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"PreCompact": [{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__COMPACT_SCRIPT__\"", "timeout": 4}]}],
|
"PreCompact": [{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__COMPACT_SCRIPT__\"", "timeout": 4}]}],
|
||||||
"SessionStart": [{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__SESSION_POINTER_SCRIPT__\"", "timeout": 4}]}],
|
"SessionStart": [
|
||||||
|
{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__SESSION_POINTER_SCRIPT__\"", "timeout": 4}]},
|
||||||
|
{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__QMD_WARM_SCRIPT__\"", "timeout": 4}]}
|
||||||
|
],
|
||||||
"Stop": [
|
"Stop": [
|
||||||
{
|
{
|
||||||
"hooks": [
|
"hooks": [
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ sandbox_mode = "workspace-write"
|
|||||||
|
|
||||||
[sandbox_workspace_write]
|
[sandbox_workspace_write]
|
||||||
network_access = true
|
network_access = true
|
||||||
writable_roots = ['__AI_CONFIG_ROOT__/../.my-ai-configuration/semantic-retrieval/data']
|
writable_roots = ['__AI_CONFIG_ROOT__/../.my-ai-configuration/qmd', '__AI_CONFIG_ROOT__/../.cache/qmd']
|
||||||
|
|
||||||
[windows]
|
[windows]
|
||||||
sandbox = "unelevated"
|
sandbox = "unelevated"
|
||||||
@@ -26,3 +26,10 @@ type = "command"
|
|||||||
command = '__HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__HOOK_SCRIPT__"'
|
command = '__HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__HOOK_SCRIPT__"'
|
||||||
command_windows = '__POWERSHELL_HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__POWERSHELL_HOOK_SCRIPT__"'
|
command_windows = '__POWERSHELL_HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__POWERSHELL_HOOK_SCRIPT__"'
|
||||||
timeout = 8
|
timeout = 8
|
||||||
|
|
||||||
|
[[hooks.SessionStart]]
|
||||||
|
[[hooks.SessionStart.hooks]]
|
||||||
|
type = "command"
|
||||||
|
command = '__HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/scripts/__QMD_WARM_SCRIPT__"'
|
||||||
|
command_windows = '__POWERSHELL_HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/scripts/__QMD_WARM_SCRIPT__"'
|
||||||
|
timeout = 4
|
||||||
@@ -209,7 +209,8 @@ foreach ($shell in @('powershell','bash')) {
|
|||||||
$allowRulesJson = ConvertTo-Json -InputObject @('Agent', "$shellTool(mcporter call context7.*)") -Compress
|
$allowRulesJson = ConvertTo-Json -InputObject @('Agent', "$shellTool(mcporter call context7.*)") -Compress
|
||||||
$compactScript = if ($shell -eq 'powershell') { 'Record-Compact.ps1' } else { 'record-compact.sh' }
|
$compactScript = if ($shell -eq 'powershell') { 'Record-Compact.ps1' } else { 'record-compact.sh' }
|
||||||
$pointerScript = if ($shell -eq 'powershell') { 'Show-SessionStatePointer.ps1' } else { 'show-session-state-pointer.sh' }
|
$pointerScript = if ($shell -eq 'powershell') { 'Show-SessionStatePointer.ps1' } else { 'show-session-state-pointer.sh' }
|
||||||
$content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__POWERSHELL_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__POWERSHELL_HOOK_SCRIPT__', $script).Replace('__COMPACT_SCRIPT__', $compactScript).Replace('__SESSION_POINTER_SCRIPT__', $pointerScript).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript).Replace('__CLAUDE_SANDBOX__', $claudeSandbox).Replace('__CLAUDE_DENY_RULES__', $denyRulesJson).Replace('__CLAUDE_ALLOW_RULES__', $allowRulesJson)
|
$qmdWarmScript = if ($shell -eq 'powershell') { 'Start-QmdWarm.ps1' } else { 'qmd-warm.sh' }
|
||||||
|
$content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__POWERSHELL_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__POWERSHELL_HOOK_SCRIPT__', $script).Replace('__COMPACT_SCRIPT__', $compactScript).Replace('__SESSION_POINTER_SCRIPT__', $pointerScript).Replace('__QMD_WARM_SCRIPT__', $qmdWarmScript).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript).Replace('__CLAUDE_SANDBOX__', $claudeSandbox).Replace('__CLAUDE_DENY_RULES__', $denyRulesJson).Replace('__CLAUDE_ALLOW_RULES__', $allowRulesJson)
|
||||||
Set-Content -LiteralPath $path -Value $content -Encoding UTF8
|
Set-Content -LiteralPath $path -Value $content -Encoding UTF8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -195,12 +195,14 @@ for client in codex claude; do
|
|||||||
statusline_script=statusline.sh
|
statusline_script=statusline.sh
|
||||||
compact_script=record-compact.sh
|
compact_script=record-compact.sh
|
||||||
pointer_script=show-session-state-pointer.sh
|
pointer_script=show-session-state-pointer.sh
|
||||||
|
qmd_warm_script=qmd-warm.sh
|
||||||
if [ "$shell" = powershell ]; then
|
if [ "$shell" = powershell ]; then
|
||||||
command='__POWERSHELL_COMMAND__'
|
command='__POWERSHELL_COMMAND__'
|
||||||
script=flashbang.ps1
|
script=flashbang.ps1
|
||||||
statusline_script=statusline.ps1
|
statusline_script=statusline.ps1
|
||||||
compact_script=Record-Compact.ps1
|
compact_script=Record-Compact.ps1
|
||||||
pointer_script=Show-SessionStatePointer.ps1
|
pointer_script=Show-SessionStatePointer.ps1
|
||||||
|
qmd_warm_script=Start-QmdWarm.ps1
|
||||||
fi
|
fi
|
||||||
path="$output/$client-$shell/$file"
|
path="$output/$client-$shell/$file"
|
||||||
content=$(<"$path")
|
content=$(<"$path")
|
||||||
@@ -217,6 +219,7 @@ for client in codex claude; do
|
|||||||
content=${content//__POWERSHELL_HOOK_SCRIPT__/$script}
|
content=${content//__POWERSHELL_HOOK_SCRIPT__/$script}
|
||||||
content=${content//__COMPACT_SCRIPT__/$compact_script}
|
content=${content//__COMPACT_SCRIPT__/$compact_script}
|
||||||
content=${content//__SESSION_POINTER_SCRIPT__/$pointer_script}
|
content=${content//__SESSION_POINTER_SCRIPT__/$pointer_script}
|
||||||
|
content=${content//__QMD_WARM_SCRIPT__/$qmd_warm_script}
|
||||||
content=${content//__STATUSLINE_COMMAND__/$command}
|
content=${content//__STATUSLINE_COMMAND__/$command}
|
||||||
content=${content//__STATUSLINE_SCRIPT__/$statusline_script}
|
content=${content//__STATUSLINE_SCRIPT__/$statusline_script}
|
||||||
content=${content//__CLAUDE_SANDBOX__/$sandbox}
|
content=${content//__CLAUDE_SANDBOX__/$sandbox}
|
||||||
|
|||||||
@@ -11,7 +11,27 @@ def is_flashbang(hook):
|
|||||||
for key in ('command', 'command_windows'))
|
for key in ('command', 'command_windows'))
|
||||||
|
|
||||||
|
|
||||||
def filter_options(path, flashbang_enabled=True, statusline_enabled=True):
|
def is_qmd_warm(hook):
|
||||||
|
return any(re.search(r'(?:qmd-warm\.sh|Start-QmdWarm\.ps1)', str(hook.get(key, '')), re.I)
|
||||||
|
for key in ('command', 'command_windows'))
|
||||||
|
|
||||||
|
|
||||||
|
def without_hooks(groups, predicate):
|
||||||
|
remaining = []
|
||||||
|
for group in groups:
|
||||||
|
hooks = [hook for hook in group.get('hooks', []) if not predicate(hook)]
|
||||||
|
if hooks:
|
||||||
|
remaining.append(dict(group, hooks=hooks))
|
||||||
|
return remaining
|
||||||
|
|
||||||
|
|
||||||
|
def filter_instructions(text, semantic_retrieval_enabled):
|
||||||
|
if semantic_retrieval_enabled:
|
||||||
|
return text
|
||||||
|
return ''.join(line for line in text.splitlines(keepends=True) if '`semantic-search`' not in line)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_options(path, flashbang_enabled=True, statusline_enabled=True, semantic_retrieval_enabled=True):
|
||||||
content = path.read_text(encoding='utf-8-sig')
|
content = path.read_text(encoding='utf-8-sig')
|
||||||
if path.name == 'settings.json':
|
if path.name == 'settings.json':
|
||||||
settings = json.loads(content)
|
settings = json.loads(content)
|
||||||
@@ -26,6 +46,12 @@ def filter_options(path, flashbang_enabled=True, statusline_enabled=True):
|
|||||||
settings['hooks']['Stop'] = remaining
|
settings['hooks']['Stop'] = remaining
|
||||||
elif 'Stop' in settings.get('hooks', {}):
|
elif 'Stop' in settings.get('hooks', {}):
|
||||||
del settings['hooks']['Stop']
|
del settings['hooks']['Stop']
|
||||||
|
if not semantic_retrieval_enabled:
|
||||||
|
remaining = without_hooks(settings.get('hooks', {}).get('SessionStart', []), is_qmd_warm)
|
||||||
|
if remaining:
|
||||||
|
settings['hooks']['SessionStart'] = remaining
|
||||||
|
else:
|
||||||
|
settings.get('hooks', {}).pop('SessionStart', None)
|
||||||
if not statusline_enabled:
|
if not statusline_enabled:
|
||||||
settings.pop('statusLine', None)
|
settings.pop('statusLine', None)
|
||||||
return json.dumps(settings, indent=2, ensure_ascii=False) + '\n'
|
return json.dumps(settings, indent=2, ensure_ascii=False) + '\n'
|
||||||
@@ -44,6 +70,17 @@ def filter_options(path, flashbang_enabled=True, statusline_enabled=True):
|
|||||||
filtered.append(section)
|
filtered.append(section)
|
||||||
result = ''.join(filtered)
|
result = ''.join(filtered)
|
||||||
result = re.sub(r'(?m)^\[\[hooks\.Stop\]\][ \t]*\r?\n\s*(?=\[|\Z)(?!\[\[hooks\.Stop\.hooks\]\])', '', result)
|
result = re.sub(r'(?m)^\[\[hooks\.Stop\]\][ \t]*\r?\n\s*(?=\[|\Z)(?!\[\[hooks\.Stop\.hooks\]\])', '', result)
|
||||||
|
if not semantic_retrieval_enabled:
|
||||||
|
sections = re.split(r'(?m)(?=^\s*\[\[?[^\r\n]+\]\]?\s*$)', result)
|
||||||
|
kept = []
|
||||||
|
for section in sections:
|
||||||
|
if re.match(r'\s*\[\[hooks\.SessionStart\.hooks\]\]', section):
|
||||||
|
hook = tomllib.loads(section)['hooks']['SessionStart']['hooks'][0]
|
||||||
|
if is_qmd_warm(hook):
|
||||||
|
continue
|
||||||
|
kept.append(section)
|
||||||
|
result = ''.join(kept)
|
||||||
|
result = re.sub(r'(?m)^\[\[hooks\.SessionStart\]\][ \t]*\r?\n\s*(?=\[|\Z)(?!\[\[hooks\.SessionStart\.hooks\]\])', '', result)
|
||||||
if not statusline_enabled:
|
if not statusline_enabled:
|
||||||
sections = re.split(r'(?m)(?=^\s*\[\[?[^\r\n]+\]\]?\s*$)', result)
|
sections = re.split(r'(?m)(?=^\s*\[\[?[^\r\n]+\]\]?\s*$)', result)
|
||||||
for index, section in enumerate(sections):
|
for index, section in enumerate(sections):
|
||||||
@@ -69,7 +106,7 @@ def merge_json(current, managed):
|
|||||||
for key, value in managed.items():
|
for key, value in managed.items():
|
||||||
if key == 'hooks' and isinstance(value, dict) and isinstance(result.get(key), dict):
|
if key == 'hooks' and isinstance(value, dict) and isinstance(result.get(key), dict):
|
||||||
hooks = dict(result[key])
|
hooks = dict(result[key])
|
||||||
for event in set(value) | {'Stop'}:
|
for event in set(value) | {'Stop', 'SessionStart'}:
|
||||||
groups = value.get(event, [])
|
groups = value.get(event, [])
|
||||||
old_groups = hooks.get(event, [])
|
old_groups = hooks.get(event, [])
|
||||||
if isinstance(groups, list) and isinstance(old_groups, list):
|
if isinstance(groups, list) and isinstance(old_groups, list):
|
||||||
@@ -79,7 +116,7 @@ def merge_json(current, managed):
|
|||||||
retained.append(group)
|
retained.append(group)
|
||||||
continue
|
continue
|
||||||
foreign = [hook for hook in group['hooks'] if not (isinstance(hook, dict) and re.search(
|
foreign = [hook for hook in group['hooks'] if not (isinstance(hook, dict) and re.search(
|
||||||
r'(?:flashbang|statusline|record-compact|session-state-pointer)\.(?:ps1|sh)',
|
r'(?:flashbang|statusline|record-compact|session-state-pointer|qmd-warm|Start-QmdWarm)\.(?:ps1|sh)',
|
||||||
str(hook.get('command', '')) + str(hook.get('command_windows', '')), re.I))]
|
str(hook.get('command', '')) + str(hook.get('command_windows', '')), re.I))]
|
||||||
if foreign:
|
if foreign:
|
||||||
retained.append(dict(group, hooks=foreign))
|
retained.append(dict(group, hooks=foreign))
|
||||||
@@ -131,7 +168,7 @@ def merge_toml(current_text, managed_text, statusline_enabled=True):
|
|||||||
for header, block in current_sections:
|
for header, block in current_sections:
|
||||||
plugin_table = re.match(r'^\[(?:plugins|marketplaces)(?:\.|\])', header)
|
plugin_table = re.match(r'^\[(?:plugins|marketplaces)(?:\.|\])', header)
|
||||||
array_table = header.startswith('[[')
|
array_table = header.startswith('[[')
|
||||||
owned_hook = any(re.search(r'(?:flashbang|statusline|record-compact|session-state-pointer)\.(?:ps1|sh)', line, re.I) for line in block)
|
owned_hook = any(re.search(r'(?:flashbang|statusline|record-compact|session-state-pointer|qmd-warm|Start-QmdWarm)\.(?:ps1|sh)', line, re.I) for line in block)
|
||||||
if plugin_table and header in managed_headers:
|
if plugin_table and header in managed_headers:
|
||||||
raise ValueError('Cannot merge conflicting managed plugin tables.')
|
raise ValueError('Cannot merge conflicting managed plugin tables.')
|
||||||
if (header not in managed_headers and not array_table) or (array_table and not owned_hook):
|
if (header not in managed_headers and not array_table) or (array_table and not owned_hook):
|
||||||
@@ -173,17 +210,20 @@ def merge_toml(current_text, managed_text, statusline_enabled=True):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('action', choices=('filter', 'merge-json', 'merge-toml'))
|
parser.add_argument('action', choices=('filter', 'filter-instructions', 'merge-json', 'merge-toml'))
|
||||||
parser.add_argument('--path', type=Path)
|
parser.add_argument('--path', type=Path)
|
||||||
parser.add_argument('--current', type=Path)
|
parser.add_argument('--current', type=Path)
|
||||||
parser.add_argument('--flashbang', choices=('true', 'false'))
|
parser.add_argument('--flashbang', choices=('true', 'false'))
|
||||||
parser.add_argument('--statusline', choices=('true', 'false'), default='true')
|
parser.add_argument('--statusline', choices=('true', 'false'), default='true')
|
||||||
|
parser.add_argument('--semantic-retrieval', choices=('true', 'false'), default='true')
|
||||||
parser.add_argument('--managed', type=Path)
|
parser.add_argument('--managed', type=Path)
|
||||||
parser.add_argument('--output', type=Path)
|
parser.add_argument('--output', type=Path)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
try:
|
try:
|
||||||
if args.action == 'filter':
|
if args.action == 'filter':
|
||||||
result = filter_options(args.path, args.flashbang == 'true', args.statusline == 'true')
|
result = filter_options(args.path, args.flashbang == 'true', args.statusline == 'true', args.semantic_retrieval == 'true')
|
||||||
|
elif args.action == 'filter-instructions':
|
||||||
|
result = filter_instructions(args.path.read_text(encoding='utf-8-sig'), args.semantic_retrieval == 'true')
|
||||||
else:
|
else:
|
||||||
managed = args.managed.read_text(encoding='utf-8-sig') if args.managed else sys.stdin.buffer.read().decode('utf-8-sig')
|
managed = args.managed.read_text(encoding='utf-8-sig') if args.managed else sys.stdin.buffer.read().decode('utf-8-sig')
|
||||||
current = args.current.read_text(encoding='utf-8-sig')
|
current = args.current.read_text(encoding='utf-8-sig')
|
||||||
|
|||||||
@@ -227,13 +227,17 @@ function Invoke-ManagedSync {
|
|||||||
if (-not $SemanticRetrievalEnabled -and $relative -match '(^|/)semantic-search/') { return }
|
if (-not $SemanticRetrievalEnabled -and $relative -match '(^|/)semantic-search/') { return }
|
||||||
$target = Resolve-ManagedPath -Destination $Destination -Relative $relative
|
$target = Resolve-ManagedPath -Destination $Destination -Relative $relative
|
||||||
$rawContent = Get-Content -LiteralPath $_.FullName -Raw -Encoding UTF8
|
$rawContent = Get-Content -LiteralPath $_.FullName -Raw -Encoding UTF8
|
||||||
$filterOptions = ((-not $FlashbangEnabled) -or (-not $StatusLineEnabled)) -and $relative -in @('settings.json', 'config.toml')
|
$filterOptions = ((-not $FlashbangEnabled) -or (-not $StatusLineEnabled) -or (-not $SemanticRetrievalEnabled)) -and $relative -in @('settings.json', 'config.toml')
|
||||||
if ($filterOptions) {
|
if ($filterOptions) {
|
||||||
$flashbangOption = if ($FlashbangEnabled) { 'true' } else { 'false' }
|
$flashbangOption = if ($FlashbangEnabled) { 'true' } else { 'false' }
|
||||||
$statusLineOption = if ($StatusLineEnabled) { 'true' } else { 'false' }
|
$statusLineOption = if ($StatusLineEnabled) { 'true' } else { 'false' }
|
||||||
$rawContent = Invoke-InstallOptions -Arguments @('filter', '--path', $_.FullName, '--flashbang', $flashbangOption, '--statusline', $statusLineOption) -ErrorMessage 'Could not configure install options.'
|
$rawContent = Invoke-InstallOptions -Arguments @('filter', '--path', $_.FullName, '--flashbang', $flashbangOption, '--statusline', $statusLineOption, '--semantic-retrieval', $(if ($SemanticRetrievalEnabled) { 'true' } else { 'false' })) -ErrorMessage 'Could not configure install options.'
|
||||||
}
|
}
|
||||||
$needsSubstitution = $filterOptions -or $rawContent.Contains('__AI_CONFIG_ROOT__') -or $rawContent.Contains('__HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_COMMAND__') -or $rawContent.Contains('__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__')
|
$filterInstructions = (-not $SemanticRetrievalEnabled) -and $relative -in @('CLAUDE.md', 'AGENTS.md', 'rules/general.md')
|
||||||
|
if ($filterInstructions) {
|
||||||
|
$rawContent = Invoke-InstallOptions -Arguments @('filter-instructions', '--path', $_.FullName, '--semantic-retrieval', 'false') -ErrorMessage 'Could not configure install options.'
|
||||||
|
}
|
||||||
|
$needsSubstitution = $filterOptions -or $filterInstructions -or $rawContent.Contains('__AI_CONFIG_ROOT__') -or $rawContent.Contains('__HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_COMMAND__') -or $rawContent.Contains('__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__')
|
||||||
if ($needsSubstitution) {
|
if ($needsSubstitution) {
|
||||||
$finalContent = $rawContent.Replace('__AI_CONFIG_ROOT__', $AiConfigRoot).Replace('__HOOK_COMMAND__', $ShellCommand).Replace('__POWERSHELL_HOOK_COMMAND__', $PowerShellCommand).Replace('__POWERSHELL_COMMAND__', $PowerShellCommand).Replace('__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__', $ClaudeConcurrency).Replace('__HOOK_SCRIPT__', 'flashbang.ps1').Replace('__POWERSHELL_HOOK_SCRIPT__', 'flashbang.ps1')
|
$finalContent = $rawContent.Replace('__AI_CONFIG_ROOT__', $AiConfigRoot).Replace('__HOOK_COMMAND__', $ShellCommand).Replace('__POWERSHELL_HOOK_COMMAND__', $PowerShellCommand).Replace('__POWERSHELL_COMMAND__', $PowerShellCommand).Replace('__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__', $ClaudeConcurrency).Replace('__HOOK_SCRIPT__', 'flashbang.ps1').Replace('__POWERSHELL_HOOK_SCRIPT__', 'flashbang.ps1')
|
||||||
$newBytes = [System.Text.Encoding]::UTF8.GetBytes($finalContent)
|
$newBytes = [System.Text.Encoding]::UTF8.GetBytes($finalContent)
|
||||||
|
|||||||
@@ -210,8 +210,13 @@ sys.stdout.buffer.write(("\n".join(lines) + "\n").encode("utf-8"))
|
|||||||
content=$(cat -- "$source_file" && printf '\034') || return
|
content=$(cat -- "$source_file" && printf '\034') || return
|
||||||
content=${content%$'\034'}
|
content=${content%$'\034'}
|
||||||
needs_sub=false
|
needs_sub=false
|
||||||
if { [ "$flashbang_enabled" = false ] || [ "$statusline_enabled" = false ]; } && [[ "$relative" = settings.json || "$relative" = config.toml ]]; then
|
if { [ "$flashbang_enabled" = false ] || [ "$statusline_enabled" = false ] || [ "$semantic_retrieval_enabled" = false ]; } && [[ "$relative" = settings.json || "$relative" = config.toml ]]; then
|
||||||
content=$(python3 "$(dirname -- "${BASH_SOURCE[0]}")/install-options.py" filter --path "$source_file" --flashbang "$flashbang_enabled" --statusline "$statusline_enabled" && printf '\034') || return
|
content=$(python3 "$(dirname -- "${BASH_SOURCE[0]}")/install-options.py" filter --path "$source_file" --flashbang "$flashbang_enabled" --statusline "$statusline_enabled" --semantic-retrieval "$semantic_retrieval_enabled" && printf '\034') || return
|
||||||
|
content=${content%$'\034'}
|
||||||
|
needs_sub=true
|
||||||
|
fi
|
||||||
|
if [ "$semantic_retrieval_enabled" = false ] && [[ "$relative" = CLAUDE.md || "$relative" = AGENTS.md || "$relative" = rules/general.md ]]; then
|
||||||
|
content=$(python3 "$(dirname -- "${BASH_SOURCE[0]}")/install-options.py" filter-instructions --path "$source_file" --semantic-retrieval false && printf '\034') || return
|
||||||
content=${content%$'\034'}
|
content=${content%$'\034'}
|
||||||
needs_sub=true
|
needs_sub=true
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -73,6 +73,31 @@ class InstallOptionsTests(unittest.TestCase):
|
|||||||
subprocess.run([sys.executable, script, 'merge-toml', '--current', str(current), '--managed', str(managed_path), '--output', str(output)], check=True)
|
subprocess.run([sys.executable, script, 'merge-toml', '--current', str(current), '--managed', str(managed_path), '--output', str(output)], check=True)
|
||||||
self.assertIn('# Größe', output.read_text(encoding='utf-8'))
|
self.assertIn('# Größe', output.read_text(encoding='utf-8'))
|
||||||
|
|
||||||
|
def test_semantic_retrieval_disabled_removes_qmd_session_hook(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
settings = Path(directory) / 'settings.json'
|
||||||
|
settings.write_text(json.dumps({'hooks': {'SessionStart': [
|
||||||
|
{'hooks': [{'type': 'command', 'command': 'bash "/c/hooks/scripts/show-session-state-pointer.sh"'}]},
|
||||||
|
{'hooks': [{'type': 'command', 'command': 'bash "/c/hooks/scripts/qmd-warm.sh"'}]}]}}), encoding='utf-8')
|
||||||
|
result = json.loads(OPTIONS.filter_options(settings, semantic_retrieval_enabled=False))
|
||||||
|
commands = [hook['command'] for group in result['hooks']['SessionStart'] for hook in group['hooks']]
|
||||||
|
self.assertEqual(commands, ['bash "/c/hooks/scripts/show-session-state-pointer.sh"'])
|
||||||
|
|
||||||
|
def test_semantic_retrieval_disabled_removes_codex_session_hook(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
config = Path(directory) / 'config.toml'
|
||||||
|
config.write_text('[features]\nhooks = true\n\n[[hooks.SessionStart]]\n[[hooks.SessionStart.hooks]]\ntype = "command"\n'
|
||||||
|
'command = \'bash "/c/hooks/scripts/qmd-warm.sh"\'\ncommand_windows = \'pwsh "/c/hooks/scripts/Start-QmdWarm.ps1"\'\ntimeout = 4\n',
|
||||||
|
encoding='utf-8')
|
||||||
|
result = OPTIONS.filter_options(config, semantic_retrieval_enabled=False)
|
||||||
|
self.assertNotIn('qmd-warm', result)
|
||||||
|
self.assertNotIn('hooks.SessionStart', result)
|
||||||
|
|
||||||
|
def test_instruction_line_only_with_semantic_search(self):
|
||||||
|
text = 'Line one.\nWhen `semantic-search` is installed, run it first.\nLine three.\n'
|
||||||
|
self.assertEqual(OPTIONS.filter_instructions(text, False), 'Line one.\nLine three.\n')
|
||||||
|
self.assertEqual(OPTIONS.filter_instructions(text, True), text)
|
||||||
|
|
||||||
def test_filter_preserves_other_claude_hooks_and_status(self):
|
def test_filter_preserves_other_claude_hooks_and_status(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
path = Path(directory) / 'settings.json'
|
path = Path(directory) / 'settings.json'
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { mkdtempSync, writeFileSync, existsSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { withLock } from '../../shared/qmd/qmd-warm.mjs';
|
||||||
|
|
||||||
|
test('a fresh lock skips the second warm-up and is released afterwards', async () => {
|
||||||
|
const lock = join(mkdtempSync(join(tmpdir(), 'qmd-warm-')), 'repo.lock');
|
||||||
|
let runs = 0;
|
||||||
|
await withLock(lock, async () => {
|
||||||
|
runs += 1;
|
||||||
|
await withLock(lock, async () => { runs += 1; });
|
||||||
|
});
|
||||||
|
assert.equal(runs, 1);
|
||||||
|
assert.equal(existsSync(lock), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a stale lock older than 30 minutes is replaced', async () => {
|
||||||
|
const lock = join(mkdtempSync(join(tmpdir(), 'qmd-warm-')), 'repo.lock');
|
||||||
|
writeFileSync(lock, String(Date.now() - 31 * 60_000));
|
||||||
|
let runs = 0;
|
||||||
|
await withLock(lock, async () => { runs += 1; });
|
||||||
|
assert.equal(runs, 1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# SessionStart: warm the QMD index and daemon in the background; never blocks or fails the session.
|
||||||
|
$ErrorActionPreference = 'SilentlyContinue'
|
||||||
|
$script = Join-Path $HOME '.my-ai-configuration/qmd/qmd-warm.mjs'
|
||||||
|
$node = Get-Command node -ErrorAction SilentlyContinue
|
||||||
|
if ((Test-Path -LiteralPath $script) -and $node) {
|
||||||
|
$null = git rev-parse --show-toplevel 2>$null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Start-Process -FilePath $node.Source -ArgumentList @("`"$script`"") -WindowStyle Hidden -WorkingDirectory (Get-Location).Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SessionStart: warm the QMD index and daemon in the background; never blocks or fails the session.
|
||||||
|
script="$HOME/.my-ai-configuration/qmd/qmd-warm.mjs"
|
||||||
|
if [ -f "$script" ] && command -v node >/dev/null 2>&1 && git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||||
|
nohup node "$script" >/dev/null 2>&1 &
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// Background warm-up started by the SessionStart hook; never fails the session.
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { appendFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { join, resolve } from 'node:path';
|
||||||
|
import { pathToFileURL } from 'node:url';
|
||||||
|
import { parseArgs } from 'node:util';
|
||||||
|
import { collectionName, readJson, stateDir } from './qmd-lib.mjs';
|
||||||
|
import { startDaemon } from './qmd-search.mjs';
|
||||||
|
|
||||||
|
const STALE_MS = 30 * 60_000;
|
||||||
|
|
||||||
|
export async function withLock(lockPath, fn, now = Date.now) {
|
||||||
|
try {
|
||||||
|
writeFileSync(lockPath, String(now()), { flag: 'wx' });
|
||||||
|
} catch {
|
||||||
|
const age = now() - Number(readFileSync(lockPath, 'utf8'));
|
||||||
|
if (!(age > STALE_MS)) return false;
|
||||||
|
writeFileSync(lockPath, String(now()));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
rmSync(lockPath, { force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh(root) {
|
||||||
|
const info = readJson(join(stateDir(), 'daemon.json'), null);
|
||||||
|
if (!info?.port) throw new Error('daemon not running');
|
||||||
|
const response = await fetch(`http://127.0.0.1:${info.port}/refresh`, { method: 'POST',
|
||||||
|
headers: { 'x-qmd-token': info.token, 'content-type': 'application/json' }, body: JSON.stringify({ repo: root }),
|
||||||
|
signal: AbortSignal.timeout(35 * 60_000) });
|
||||||
|
if (!response.ok) throw new Error(`refresh answered ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { values } = parseArgs({ options: { root: { type: 'string' } } });
|
||||||
|
const root = resolve(values.root || execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim());
|
||||||
|
const dir = stateDir();
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
// A refresh through the daemon indexes the repository incrementally; start the daemon first if needed.
|
||||||
|
await withLock(join(dir, `${collectionName(root)}.lock`), async () => {
|
||||||
|
try {
|
||||||
|
await refresh(root);
|
||||||
|
} catch {
|
||||||
|
try { await startDaemon(); } catch { /* another daemon may have won the start race */ }
|
||||||
|
await refresh(root);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
|
||||||
|
main().catch((error) => {
|
||||||
|
try { appendFileSync(join(stateDir(), 'warm.log'), `${new Date().toISOString()} ${error.message}\n`); } catch { /* ignore */ }
|
||||||
|
}).finally(() => process.exit(0));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user