aiwcli 0.10.3 → 0.11.1
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.
- package/bin/run.js +1 -1
- package/dist/commands/clear.js +28 -131
- package/dist/commands/init/index.js +3 -3
- package/dist/lib/gitignore-manager.d.ts +32 -0
- package/dist/lib/gitignore-manager.js +141 -2
- package/dist/templates/CLAUDE.md +8 -8
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
- package/dist/templates/_shared/.claude/settings.json +7 -7
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
- package/dist/templates/_shared/hooks-ts/session_end.ts +107 -0
- package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
- package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
- package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
- package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
- package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
- package/dist/templates/_shared/lib-ts/base/logger.ts +15 -2
- package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +142 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
- package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +43 -23
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
- package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
- package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +158 -0
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
- package/dist/templates/_shared/lib-ts/types.ts +68 -55
- package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
- package/dist/templates/_shared/scripts/resume_handoff.ts +345 -0
- package/dist/templates/_shared/scripts/save_handoff.ts +3 -3
- package/dist/templates/_shared/scripts/status_line.ts +687 -0
- package/dist/templates/cc-native/.claude/settings.json +175 -185
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1027 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +792 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +120 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +250 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +275 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +107 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +240 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +385 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +14 -1
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
- package/dist/templates/_shared/hooks/__init__.py +0 -16
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +0 -177
- package/dist/templates/_shared/hooks/context_monitor.py +0 -270
- package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
- package/dist/templates/_shared/hooks/pre_compact.py +0 -104
- package/dist/templates/_shared/hooks/session_end.py +0 -173
- package/dist/templates/_shared/hooks/session_start.py +0 -206
- package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
- package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
- package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
- package/dist/templates/_shared/lib/__init__.py +0 -1
- package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__init__.py +0 -65
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
- package/dist/templates/_shared/lib/base/constants.py +0 -358
- package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
- package/dist/templates/_shared/lib/base/inference.py +0 -307
- package/dist/templates/_shared/lib/base/logger.py +0 -305
- package/dist/templates/_shared/lib/base/stop_words.py +0 -221
- package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
- package/dist/templates/_shared/lib/base/utils.py +0 -263
- package/dist/templates/_shared/lib/context/__init__.py +0 -102
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
- package/dist/templates/_shared/lib/context/context_selector.py +0 -508
- package/dist/templates/_shared/lib/context/context_store.py +0 -653
- package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
- package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
- package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
- package/dist/templates/_shared/lib/templates/README.md +0 -206
- package/dist/templates/_shared/lib/templates/__init__.py +0 -36
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/formatters.py +0 -146
- package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +0 -357
- package/dist/templates/_shared/scripts/status_line.py +0 -716
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/MIGRATION.md +0 -86
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
- package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
- package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
- package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
- package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
"""Common utilities for hook scripts.
|
|
2
|
-
|
|
3
|
-
Provides standardized boilerplate for:
|
|
4
|
-
- Path setup for imports
|
|
5
|
-
- JSON parsing from stdin
|
|
6
|
-
- Hook payload validation
|
|
7
|
-
- Error handling decorators
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import os
|
|
12
|
-
import sys
|
|
13
|
-
from datetime import datetime, timezone
|
|
14
|
-
from functools import wraps
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Any, Callable, Dict, Optional, TypeVar
|
|
17
|
-
|
|
18
|
-
from .logger import log_hook_error, hook_log, log_debug, log_info, log_warn, log_error, log_diagnostic, set_context_path, set_session_id
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# Context window baseline: tokens not visible in hook data
|
|
22
|
-
# (system prompt, tools, MCP tokens)
|
|
23
|
-
# See: https://github.com/anthropics/claude-code/issues/13783
|
|
24
|
-
CONTEXT_BASELINE_TOKENS = 22_600
|
|
25
|
-
DEFAULT_CONTEXT_WINDOW_SIZE = 200_000
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def parse_context_window(hook_input: dict) -> tuple:
|
|
29
|
-
"""Parse context window from hook input.
|
|
30
|
-
|
|
31
|
-
Returns (tokens_used, max_tokens) or (None, None).
|
|
32
|
-
tokens_used includes baseline offset for system prompt/tools.
|
|
33
|
-
"""
|
|
34
|
-
context_window = hook_input.get("context_window")
|
|
35
|
-
if not context_window:
|
|
36
|
-
return None, None
|
|
37
|
-
current_usage = context_window.get("current_usage")
|
|
38
|
-
if not current_usage:
|
|
39
|
-
return None, None
|
|
40
|
-
cache_read = current_usage.get("cache_read_input_tokens", 0) or 0
|
|
41
|
-
input_tokens = current_usage.get("input_tokens", 0) or 0
|
|
42
|
-
cache_creation = current_usage.get("cache_creation_input_tokens", 0) or 0
|
|
43
|
-
output_tokens = current_usage.get("output_tokens", 0) or 0
|
|
44
|
-
content_tokens = cache_read + input_tokens + cache_creation + output_tokens
|
|
45
|
-
tokens_used = content_tokens + CONTEXT_BASELINE_TOKENS
|
|
46
|
-
max_tokens = context_window.get("context_window_size") or DEFAULT_CONTEXT_WINDOW_SIZE
|
|
47
|
-
return tokens_used, max_tokens
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def get_context_percent_remaining(hook_input: dict) -> tuple:
|
|
51
|
-
"""Get context percentage remaining with context.json fallback.
|
|
52
|
-
|
|
53
|
-
Tries two sources in order:
|
|
54
|
-
1. Hook input context_window data (most accurate, real-time)
|
|
55
|
-
2. context.json remaining_percentage (written by status_line.py)
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
(percent_remaining, tokens_used, max_tokens) where tokens_used and
|
|
59
|
-
max_tokens may be None if data came from context.json fallback.
|
|
60
|
-
Returns (None, None, None) if no data available from either source.
|
|
61
|
-
"""
|
|
62
|
-
# Source 1: Hook input (most accurate)
|
|
63
|
-
tokens_used, max_tokens = parse_context_window(hook_input)
|
|
64
|
-
if tokens_used is not None and max_tokens is not None and max_tokens > 0:
|
|
65
|
-
remaining = max_tokens - tokens_used
|
|
66
|
-
percent_remaining = max(0, min(100, int((remaining / max_tokens) * 100)))
|
|
67
|
-
return percent_remaining, tokens_used, max_tokens
|
|
68
|
-
|
|
69
|
-
# Source 2: context.json fallback (written by status_line.py)
|
|
70
|
-
try:
|
|
71
|
-
from .utils import project_dir
|
|
72
|
-
from ..context.context_store import get_context_by_session_id
|
|
73
|
-
|
|
74
|
-
session_id = hook_input.get("session_id")
|
|
75
|
-
if session_id:
|
|
76
|
-
project_root = project_dir(hook_input)
|
|
77
|
-
context = get_context_by_session_id(session_id, project_root)
|
|
78
|
-
if context and context.last_session:
|
|
79
|
-
pct = context.last_session.get("context_remaining_pct")
|
|
80
|
-
if pct is not None:
|
|
81
|
-
return pct, None, None
|
|
82
|
-
except Exception:
|
|
83
|
-
pass # Fallback failed — degrade gracefully
|
|
84
|
-
|
|
85
|
-
return None, None, None
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# Type variable for generic decorators
|
|
89
|
-
F = TypeVar('F', bound=Callable[..., Any])
|
|
90
|
-
|
|
91
|
-
# Event metadata stash — populated by load_hook_input(), read by run_hook()
|
|
92
|
-
_last_hook_event: Optional[str] = None
|
|
93
|
-
_last_tool_name: Optional[str] = None
|
|
94
|
-
_last_session_id: Optional[str] = None
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def load_hook_input() -> Optional[Dict[str, Any]]:
|
|
98
|
-
"""
|
|
99
|
-
Load and parse JSON from stdin.
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
Parsed JSON dict, or None if stdin is empty or invalid JSON
|
|
103
|
-
"""
|
|
104
|
-
global _last_hook_event, _last_tool_name, _last_session_id
|
|
105
|
-
try:
|
|
106
|
-
input_data = sys.stdin.read().strip()
|
|
107
|
-
if not input_data:
|
|
108
|
-
return None
|
|
109
|
-
result = json.loads(input_data)
|
|
110
|
-
if isinstance(result, dict):
|
|
111
|
-
_last_hook_event = result.get("hook_event_name")
|
|
112
|
-
_last_tool_name = result.get("tool_name")
|
|
113
|
-
_last_session_id = result.get("session_id")
|
|
114
|
-
return result
|
|
115
|
-
except json.JSONDecodeError:
|
|
116
|
-
return None
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def validate_hook_event(
|
|
120
|
-
payload: Dict[str, Any],
|
|
121
|
-
expected_event: str,
|
|
122
|
-
expected_tool: Optional[str] = None
|
|
123
|
-
) -> bool:
|
|
124
|
-
"""
|
|
125
|
-
Validate hook event type and optional tool name.
|
|
126
|
-
|
|
127
|
-
Args:
|
|
128
|
-
payload: Hook payload from stdin
|
|
129
|
-
expected_event: Expected hook_event_name (e.g., "PostToolUse", "PreToolUse")
|
|
130
|
-
expected_tool: Optional expected tool_name (e.g., "TaskCreate")
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
True if payload matches expected event/tool, False otherwise
|
|
134
|
-
"""
|
|
135
|
-
if payload.get("hook_event_name") != expected_event:
|
|
136
|
-
return False
|
|
137
|
-
if expected_tool and payload.get("tool_name") != expected_tool:
|
|
138
|
-
return False
|
|
139
|
-
return True
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def get_tool_input(payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
143
|
-
"""
|
|
144
|
-
Extract and validate tool_input from payload.
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
payload: Hook payload from stdin
|
|
148
|
-
|
|
149
|
-
Returns:
|
|
150
|
-
tool_input dict, or None if missing/invalid
|
|
151
|
-
"""
|
|
152
|
-
tool_input = payload.get("tool_input", {})
|
|
153
|
-
return tool_input if isinstance(tool_input, dict) else None
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def check_skip_persistence(payload: Dict[str, Any], hook_name: str = "hook") -> bool:
|
|
157
|
-
"""
|
|
158
|
-
Check if persistence should be skipped based on metadata flags.
|
|
159
|
-
|
|
160
|
-
Args:
|
|
161
|
-
payload: Hook payload from stdin
|
|
162
|
-
hook_name: Name of hook for logging
|
|
163
|
-
|
|
164
|
-
Returns:
|
|
165
|
-
True if skip_persistence flag is set, False otherwise
|
|
166
|
-
"""
|
|
167
|
-
tool_input = get_tool_input(payload)
|
|
168
|
-
if not tool_input:
|
|
169
|
-
return False
|
|
170
|
-
|
|
171
|
-
metadata = tool_input.get("metadata", {})
|
|
172
|
-
if isinstance(metadata, dict) and metadata.get("skip_persistence"):
|
|
173
|
-
log_debug(hook_name, "Skipping persistence (skip_persistence flag set)")
|
|
174
|
-
return True
|
|
175
|
-
return False
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def safe_hook_main(hook_name: str) -> Callable[[F], F]:
|
|
179
|
-
"""
|
|
180
|
-
Decorator for hook main functions with standard error handling.
|
|
181
|
-
|
|
182
|
-
Catches exceptions, logs them to stderr, and returns 0 (non-blocking).
|
|
183
|
-
|
|
184
|
-
Args:
|
|
185
|
-
hook_name: Name of hook for error messages
|
|
186
|
-
|
|
187
|
-
Returns:
|
|
188
|
-
Decorator function
|
|
189
|
-
|
|
190
|
-
Example:
|
|
191
|
-
@safe_hook_main("my_hook")
|
|
192
|
-
def main() -> int:
|
|
193
|
-
# ... hook logic ...
|
|
194
|
-
return 0
|
|
195
|
-
"""
|
|
196
|
-
def decorator(func: F) -> F:
|
|
197
|
-
@wraps(func)
|
|
198
|
-
def wrapper(*args, **kwargs):
|
|
199
|
-
try:
|
|
200
|
-
return func(*args, **kwargs)
|
|
201
|
-
except json.JSONDecodeError as e:
|
|
202
|
-
import traceback
|
|
203
|
-
tb = traceback.format_exc()
|
|
204
|
-
log_hook_error(hook_name, e, traceback_str=tb)
|
|
205
|
-
log_error(hook_name, f"JSON decode error: {e}")
|
|
206
|
-
return 0
|
|
207
|
-
except Exception as e:
|
|
208
|
-
import traceback
|
|
209
|
-
tb = traceback.format_exc()
|
|
210
|
-
log_hook_error(hook_name, e, traceback_str=tb)
|
|
211
|
-
log_error(hook_name, f"Unexpected error: {e}", traceback_str=tb)
|
|
212
|
-
return 0
|
|
213
|
-
return wrapper # type: ignore
|
|
214
|
-
return decorator
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def emit_context(additional_context: str, ensure_ascii: bool = False) -> None:
|
|
218
|
-
"""Emit hookSpecificOutput with additionalContext to stdout.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
additional_context: Context string to inject into Claude's context
|
|
222
|
-
ensure_ascii: If True, escape non-ASCII characters in JSON output
|
|
223
|
-
"""
|
|
224
|
-
out = {
|
|
225
|
-
"hookSpecificOutput": {
|
|
226
|
-
"additionalContext": additional_context,
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
print(json.dumps(out, ensure_ascii=ensure_ascii))
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def emit_context_and_block(
|
|
233
|
-
additional_context: str,
|
|
234
|
-
reason: str,
|
|
235
|
-
ensure_ascii: bool = True,
|
|
236
|
-
) -> None:
|
|
237
|
-
"""Emit hookSpecificOutput that denies the tool call with context and reason.
|
|
238
|
-
|
|
239
|
-
Args:
|
|
240
|
-
additional_context: Context string to inject into Claude's context
|
|
241
|
-
reason: Reason shown to Claude for why the tool call was denied
|
|
242
|
-
ensure_ascii: If True, escape non-ASCII characters in JSON output
|
|
243
|
-
"""
|
|
244
|
-
out = {
|
|
245
|
-
"hookSpecificOutput": {
|
|
246
|
-
"additionalContext": additional_context,
|
|
247
|
-
"permissionDecision": "deny",
|
|
248
|
-
"permissionDecisionReason": reason,
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
print(json.dumps(out, ensure_ascii=ensure_ascii))
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def _detect_template(script_path: str = "") -> str:
|
|
255
|
-
"""Auto-detect template origin from the hook script path.
|
|
256
|
-
|
|
257
|
-
Returns "shared", a template name (e.g., "cc-native"), or "unknown".
|
|
258
|
-
"""
|
|
259
|
-
import re
|
|
260
|
-
path = (script_path or (sys.argv[0] if sys.argv else "")).replace("\\", "/")
|
|
261
|
-
if "/_shared/hooks/" in path or path.startswith("_shared/hooks/"):
|
|
262
|
-
return "shared"
|
|
263
|
-
match = re.search(r'_([a-z][a-z0-9-]*)/hooks/', path)
|
|
264
|
-
if match:
|
|
265
|
-
return match.group(1) # e.g., "cc-native"
|
|
266
|
-
return "unknown"
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def run_hook(main_func: Callable[[], int], hook_name: str = "unknown") -> None:
|
|
270
|
-
"""
|
|
271
|
-
Standard hook entry point wrapper with lifecycle logging.
|
|
272
|
-
|
|
273
|
-
Logs HOOK_START before calling main, HOOK_END after completion.
|
|
274
|
-
Catches unhandled exceptions and logs them before exiting cleanly.
|
|
275
|
-
|
|
276
|
-
Args:
|
|
277
|
-
main_func: Hook main function that returns exit code
|
|
278
|
-
hook_name: Name of the hook for error logging
|
|
279
|
-
|
|
280
|
-
Example:
|
|
281
|
-
if __name__ == "__main__":
|
|
282
|
-
run_hook(main, "my_hook")
|
|
283
|
-
"""
|
|
284
|
-
import time
|
|
285
|
-
start_time = time.monotonic()
|
|
286
|
-
template = _detect_template()
|
|
287
|
-
event = _last_hook_event or "unknown"
|
|
288
|
-
tool = _last_tool_name
|
|
289
|
-
|
|
290
|
-
# Wire session_id into logger so all log entries carry it
|
|
291
|
-
if _last_session_id:
|
|
292
|
-
set_session_id(_last_session_id)
|
|
293
|
-
|
|
294
|
-
# HOOK_START
|
|
295
|
-
start_data: Dict[str, Any] = {"lifecycle": "start", "template": template, "event": event}
|
|
296
|
-
if tool:
|
|
297
|
-
start_data["tool"] = tool
|
|
298
|
-
log_info(hook_name, "HOOK_START", data=start_data)
|
|
299
|
-
|
|
300
|
-
exit_code = 0
|
|
301
|
-
status = "success"
|
|
302
|
-
error_info = None
|
|
303
|
-
|
|
304
|
-
try:
|
|
305
|
-
result = main_func()
|
|
306
|
-
exit_code = result if isinstance(result, int) else 0
|
|
307
|
-
status = "blocked" if exit_code != 0 else "success"
|
|
308
|
-
except SystemExit as e:
|
|
309
|
-
exit_code = e.code if isinstance(e.code, int) else (1 if e.code else 0)
|
|
310
|
-
status = "blocked" if exit_code != 0 else "success"
|
|
311
|
-
except Exception as e:
|
|
312
|
-
import traceback
|
|
313
|
-
exit_code = 0 # Non-blocking
|
|
314
|
-
status = "error"
|
|
315
|
-
error_info = (e, traceback.format_exc())
|
|
316
|
-
|
|
317
|
-
# HOOK_END
|
|
318
|
-
duration_ms = round((time.monotonic() - start_time) * 1000, 1)
|
|
319
|
-
end_data: Dict[str, Any] = {
|
|
320
|
-
"lifecycle": "end", "status": status,
|
|
321
|
-
"duration_ms": duration_ms, "exit_code": exit_code,
|
|
322
|
-
"template": template,
|
|
323
|
-
}
|
|
324
|
-
end_event = _last_hook_event or event # Re-read after main() populated it
|
|
325
|
-
end_tool = _last_tool_name or tool
|
|
326
|
-
end_data["event"] = end_event
|
|
327
|
-
if end_tool:
|
|
328
|
-
end_data["tool"] = end_tool
|
|
329
|
-
if error_info:
|
|
330
|
-
e, tb = error_info
|
|
331
|
-
end_data["error_type"] = type(e).__name__
|
|
332
|
-
log_hook_error(hook_name, e, traceback_str=tb)
|
|
333
|
-
log_error(hook_name, f"HOOK_END: {e}", data=end_data, traceback_str=tb)
|
|
334
|
-
elif status == "blocked":
|
|
335
|
-
log_warn(hook_name, "HOOK_END", data=end_data)
|
|
336
|
-
else:
|
|
337
|
-
log_info(hook_name, "HOOK_END", data=end_data)
|
|
338
|
-
|
|
339
|
-
raise SystemExit(exit_code)
|
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
"""Inference utility for AI-powered text processing.
|
|
2
|
-
|
|
3
|
-
Provides a unified interface for Claude API calls using the claude CLI.
|
|
4
|
-
Supports multiple model tiers: fast (Haiku), standard (Sonnet), smart (Opus).
|
|
5
|
-
"""
|
|
6
|
-
import json
|
|
7
|
-
import re
|
|
8
|
-
import subprocess
|
|
9
|
-
import sys
|
|
10
|
-
import os
|
|
11
|
-
from typing import Optional
|
|
12
|
-
|
|
13
|
-
from .logger import log_debug, log_info, log_warn, log_error
|
|
14
|
-
from dataclasses import dataclass
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass
|
|
18
|
-
class InferenceResult:
|
|
19
|
-
"""Result from an inference call."""
|
|
20
|
-
success: bool
|
|
21
|
-
output: str
|
|
22
|
-
error: Optional[str] = None
|
|
23
|
-
latency_ms: int = 0
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# Model configurations
|
|
27
|
-
MODELS = {
|
|
28
|
-
"fast": "claude-3-haiku-20240307",
|
|
29
|
-
"standard": "claude-sonnet-4-20250514",
|
|
30
|
-
"smart": "claude-opus-4-20250514",
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
TIMEOUTS = {
|
|
34
|
-
"fast": 15, # 15 seconds
|
|
35
|
-
"standard": 30, # 30 seconds
|
|
36
|
-
"smart": 90, # 90 seconds
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def inference(
|
|
41
|
-
system_prompt: str,
|
|
42
|
-
user_prompt: str,
|
|
43
|
-
level: str = "fast",
|
|
44
|
-
timeout: Optional[int] = None,
|
|
45
|
-
) -> InferenceResult:
|
|
46
|
-
"""
|
|
47
|
-
Run inference using the claude CLI.
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
system_prompt: System instructions for the model
|
|
51
|
-
user_prompt: User message to process
|
|
52
|
-
level: Model level - "fast" (Haiku), "standard" (Sonnet), "smart" (Opus)
|
|
53
|
-
timeout: Custom timeout in seconds (uses level default if not specified)
|
|
54
|
-
|
|
55
|
-
Returns:
|
|
56
|
-
InferenceResult with success status, output, and any error
|
|
57
|
-
"""
|
|
58
|
-
import time
|
|
59
|
-
start_time = time.time()
|
|
60
|
-
|
|
61
|
-
model = MODELS.get(level, MODELS["fast"])
|
|
62
|
-
timeout_sec = timeout or TIMEOUTS.get(level, TIMEOUTS["fast"])
|
|
63
|
-
|
|
64
|
-
# Combine prompts
|
|
65
|
-
full_prompt = f"{system_prompt}\n\n{user_prompt}"
|
|
66
|
-
|
|
67
|
-
# Build command
|
|
68
|
-
cmd = [
|
|
69
|
-
"claude",
|
|
70
|
-
"--model", model,
|
|
71
|
-
"--print",
|
|
72
|
-
"--setting-sources", "",
|
|
73
|
-
"-p", full_prompt,
|
|
74
|
-
]
|
|
75
|
-
|
|
76
|
-
# Remove ANTHROPIC_API_KEY to force subscription auth
|
|
77
|
-
env = os.environ.copy()
|
|
78
|
-
env.pop("ANTHROPIC_API_KEY", None)
|
|
79
|
-
|
|
80
|
-
try:
|
|
81
|
-
result = subprocess.run(
|
|
82
|
-
cmd,
|
|
83
|
-
capture_output=True,
|
|
84
|
-
text=True,
|
|
85
|
-
timeout=timeout_sec,
|
|
86
|
-
env=env,
|
|
87
|
-
# Windows needs shell=True for command resolution
|
|
88
|
-
shell=(sys.platform == "win32"),
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
latency_ms = int((time.time() - start_time) * 1000)
|
|
92
|
-
|
|
93
|
-
if result.returncode != 0:
|
|
94
|
-
return InferenceResult(
|
|
95
|
-
success=False,
|
|
96
|
-
output=result.stdout.strip() if result.stdout else "",
|
|
97
|
-
error=result.stderr.strip() if result.stderr else f"Exit code: {result.returncode}",
|
|
98
|
-
latency_ms=latency_ms,
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
return InferenceResult(
|
|
102
|
-
success=True,
|
|
103
|
-
output=result.stdout.strip(),
|
|
104
|
-
latency_ms=latency_ms,
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
except subprocess.TimeoutExpired:
|
|
108
|
-
latency_ms = int((time.time() - start_time) * 1000)
|
|
109
|
-
return InferenceResult(
|
|
110
|
-
success=False,
|
|
111
|
-
output="",
|
|
112
|
-
error=f"Timeout after {timeout_sec}s",
|
|
113
|
-
latency_ms=latency_ms,
|
|
114
|
-
)
|
|
115
|
-
except FileNotFoundError:
|
|
116
|
-
latency_ms = int((time.time() - start_time) * 1000)
|
|
117
|
-
return InferenceResult(
|
|
118
|
-
success=False,
|
|
119
|
-
output="",
|
|
120
|
-
error="claude CLI not found",
|
|
121
|
-
latency_ms=latency_ms,
|
|
122
|
-
)
|
|
123
|
-
except Exception as e:
|
|
124
|
-
latency_ms = int((time.time() - start_time) * 1000)
|
|
125
|
-
return InferenceResult(
|
|
126
|
-
success=False,
|
|
127
|
-
output="",
|
|
128
|
-
error=str(e),
|
|
129
|
-
latency_ms=latency_ms,
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
# Stop words for filtering (from corpus analysis of 1,424 documents)
|
|
134
|
-
from .stop_words import STOP_WORDS
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def filter_stop_words(text: str) -> str:
|
|
138
|
-
"""Remove stop words from text, keeping only content keywords."""
|
|
139
|
-
from .utils import clean_text_for_slug
|
|
140
|
-
cleaned = clean_text_for_slug(text)
|
|
141
|
-
words = cleaned.split()
|
|
142
|
-
filtered = [w for w in words if w not in STOP_WORDS and len(w) > 1]
|
|
143
|
-
return ' '.join(filtered)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# System prompt for generating context ID summaries (keyword extraction for recognition)
|
|
147
|
-
CONTEXT_ID_SYSTEM_PROMPT = """Extract 6-12 keywords from what the user wants to do.
|
|
148
|
-
|
|
149
|
-
Rules:
|
|
150
|
-
- Output 6-12 keywords only
|
|
151
|
-
- Keywords: nouns, verbs, adjectives, technical terms, proper names
|
|
152
|
-
- NO function words: the, to, with, for, in, a, an, of, on, is, it, and, or, that, this, be, as, at, by, from
|
|
153
|
-
- Most important/specific words preferred
|
|
154
|
-
- No punctuation, no quotes
|
|
155
|
-
|
|
156
|
-
Examples:
|
|
157
|
-
- "I want to add user authentication" -> "add user authentication login security JWT tokens webapp service"
|
|
158
|
-
- "Fix the bug in the login flow" -> "fix bug login flow validation error redirect session auth handler"
|
|
159
|
-
- "Can you help me refactor this code" -> "refactor code cleanup architecture maintainability legacy modules structure patterns"
|
|
160
|
-
- "Update the README with new instructions" -> "update README documentation instructions setup configuration install guide steps"
|
|
161
|
-
|
|
162
|
-
Output ONLY the keywords separated by spaces, nothing else."""
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def generate_semantic_summary(prompt: str, timeout: int = 15) -> Optional[str]:
|
|
166
|
-
"""
|
|
167
|
-
Generate a keyword summary of a user prompt.
|
|
168
|
-
|
|
169
|
-
Uses Sonnet for quality inference. Returns None if inference fails.
|
|
170
|
-
|
|
171
|
-
Args:
|
|
172
|
-
prompt: User prompt to summarize
|
|
173
|
-
timeout: Timeout in seconds (default 15)
|
|
174
|
-
|
|
175
|
-
Returns:
|
|
176
|
-
Keyword summary string (5-10 words) or None if failed
|
|
177
|
-
"""
|
|
178
|
-
result = inference(
|
|
179
|
-
system_prompt=CONTEXT_ID_SYSTEM_PROMPT,
|
|
180
|
-
user_prompt=prompt,
|
|
181
|
-
level="standard",
|
|
182
|
-
timeout=timeout,
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
if not result.success or not result.output:
|
|
186
|
-
return None
|
|
187
|
-
|
|
188
|
-
# Clean up the output
|
|
189
|
-
summary = result.output.strip()
|
|
190
|
-
# Remove any quotes
|
|
191
|
-
summary = summary.strip('"\'')
|
|
192
|
-
# Remove trailing punctuation
|
|
193
|
-
summary = summary.rstrip('.!?')
|
|
194
|
-
|
|
195
|
-
# Filter stop words
|
|
196
|
-
summary = filter_stop_words(summary)
|
|
197
|
-
|
|
198
|
-
# Validate 6-12 words for sufficient context
|
|
199
|
-
words = summary.split()
|
|
200
|
-
if len(words) < 6 or len(words) > 12:
|
|
201
|
-
return None
|
|
202
|
-
|
|
203
|
-
return summary
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
# System prompt for generating context ID slugs (8-12 word summary phrases for folder names)
|
|
207
|
-
CONTEXT_ID_SLUG_PROMPT = """You generate short title phrases for work sessions. These become folder names like `260206-1959-fix-auth-middleware-redirect-loop-session-timeout`.
|
|
208
|
-
|
|
209
|
-
Users scan 100+ such names to find past sessions. Your title must make THIS session instantly recognizable.
|
|
210
|
-
|
|
211
|
-
Rules:
|
|
212
|
-
- Exactly 8-12 lowercase words
|
|
213
|
-
- First word is an action verb (fix, add, implement, refactor, update, create, remove, optimize, debug, migrate, integrate, configure, deploy, scaffold, restructure)
|
|
214
|
-
- Coherent phrase, not disjointed keywords — reads like a short task description
|
|
215
|
-
- Prefer specific technical terms over generic words
|
|
216
|
-
- No articles (the, a, an), no pronouns, no filler words, no punctuation, no quotes
|
|
217
|
-
- Input may come from speech-to-text with filler words (uh, um, like, you know, basically, so) — ignore them entirely
|
|
218
|
-
|
|
219
|
-
Examples:
|
|
220
|
-
|
|
221
|
-
Input: "um so basically I need to like fix the auth bug in the login page"
|
|
222
|
-
{"slug": "fix authentication bug login page redirect session handling flow"}
|
|
223
|
-
|
|
224
|
-
Input: "hey uh can we add dark mode to the settings page"
|
|
225
|
-
{"slug": "add dark mode toggle settings page user preference storage"}
|
|
226
|
-
|
|
227
|
-
Input: "the context ids are bad can we change how we generate them towards a summary"
|
|
228
|
-
{"slug": "improve context id generation use prompt summary slugs"}
|
|
229
|
-
|
|
230
|
-
Input: "I want to refactor the database connection pooling for PostgreSQL"
|
|
231
|
-
{"slug": "refactor postgresql database connection pooling optimize query performance"}
|
|
232
|
-
|
|
233
|
-
Input: "so like you know the webhook retry logic is broken and stuff"
|
|
234
|
-
{"slug": "fix webhook retry logic broken error handling recovery mechanism"}
|
|
235
|
-
|
|
236
|
-
Input: "update the CI pipeline to cache node modules between runs"
|
|
237
|
-
{"slug": "update ci pipeline cache node modules between workflow runs"}
|
|
238
|
-
|
|
239
|
-
Respond with ONLY a JSON object: {"slug": "your 8-12 word phrase here"}"""
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def generate_context_id_slug(prompt: str, timeout: int = 5) -> Optional[str]:
|
|
243
|
-
"""
|
|
244
|
-
Generate a 5-12 word context ID slug from a user prompt using AI inference.
|
|
245
|
-
|
|
246
|
-
Uses Haiku (fast tier) for low-latency summary generation within hook timeout budgets.
|
|
247
|
-
Prompts for JSON output {"slug": "..."} with fallback to raw text parsing.
|
|
248
|
-
|
|
249
|
-
Args:
|
|
250
|
-
prompt: Raw user prompt to summarize (may include STT filler words)
|
|
251
|
-
timeout: Timeout in seconds (default 3, fits within 5-10s hook budget)
|
|
252
|
-
|
|
253
|
-
Returns:
|
|
254
|
-
Space-separated summary slug (5-12 words) or None if failed
|
|
255
|
-
"""
|
|
256
|
-
# Truncate input to 500 chars to keep inference fast
|
|
257
|
-
truncated = prompt[:500] if len(prompt) > 500 else prompt
|
|
258
|
-
|
|
259
|
-
result = inference(
|
|
260
|
-
system_prompt=CONTEXT_ID_SLUG_PROMPT,
|
|
261
|
-
user_prompt=truncated,
|
|
262
|
-
level="fast",
|
|
263
|
-
timeout=timeout,
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
if not result.success or not result.output:
|
|
267
|
-
log_warn("inference", f"Context ID slug inference failed: {result.error}")
|
|
268
|
-
return None
|
|
269
|
-
|
|
270
|
-
raw = result.output.strip()
|
|
271
|
-
|
|
272
|
-
# Parse JSON response {"slug": "..."}, fall back to raw text
|
|
273
|
-
slug = None
|
|
274
|
-
try:
|
|
275
|
-
parsed = json.loads(raw)
|
|
276
|
-
if isinstance(parsed, dict) and "slug" in parsed:
|
|
277
|
-
slug = parsed["slug"]
|
|
278
|
-
except (json.JSONDecodeError, TypeError):
|
|
279
|
-
pass
|
|
280
|
-
|
|
281
|
-
if not slug:
|
|
282
|
-
# Fallback: treat entire output as raw text
|
|
283
|
-
slug = raw
|
|
284
|
-
|
|
285
|
-
# Clean: strip quotes, punctuation, hyphens
|
|
286
|
-
slug = slug.strip('"\'`')
|
|
287
|
-
slug = slug.rstrip('.!?')
|
|
288
|
-
slug = slug.replace('-', ' ')
|
|
289
|
-
|
|
290
|
-
# Remove non-alphanumeric chars (except spaces)
|
|
291
|
-
slug = re.sub(r'[^a-zA-Z0-9 ]', '', slug)
|
|
292
|
-
|
|
293
|
-
# Normalize whitespace
|
|
294
|
-
slug = re.sub(r'\s+', ' ', slug).strip()
|
|
295
|
-
|
|
296
|
-
words = slug.split()
|
|
297
|
-
|
|
298
|
-
# Validate word count: truncate if over 12, reject if under 5
|
|
299
|
-
if len(words) > 12:
|
|
300
|
-
words = words[:12]
|
|
301
|
-
if len(words) < 5:
|
|
302
|
-
log_debug("inference", f"Context ID slug too short ({len(words)} words): '{slug}'")
|
|
303
|
-
return None
|
|
304
|
-
|
|
305
|
-
result_slug = ' '.join(words)
|
|
306
|
-
log_debug("inference", f"Generated context ID slug: '{result_slug}' ({result.latency_ms}ms)")
|
|
307
|
-
return result_slug
|