codeforge-dev 1.4.0
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/.devcontainer/.env +22 -0
- package/.devcontainer/CHANGELOG.md +197 -0
- package/.devcontainer/CLAUDE.md +117 -0
- package/.devcontainer/README.md +222 -0
- package/.devcontainer/config/main-system-prompt.md +502 -0
- package/.devcontainer/config/settings.json +47 -0
- package/.devcontainer/devcontainer.json +94 -0
- package/.devcontainer/features/README.md +113 -0
- package/.devcontainer/features/agent-browser/README.md +65 -0
- package/.devcontainer/features/agent-browser/devcontainer-feature.json +23 -0
- package/.devcontainer/features/agent-browser/install.sh +79 -0
- package/.devcontainer/features/ast-grep/README.md +24 -0
- package/.devcontainer/features/ast-grep/devcontainer-feature.json +24 -0
- package/.devcontainer/features/ast-grep/install.sh +51 -0
- package/.devcontainer/features/ccstatusline/README.md +296 -0
- package/.devcontainer/features/ccstatusline/devcontainer-feature.json +19 -0
- package/.devcontainer/features/ccstatusline/install.sh +290 -0
- package/.devcontainer/features/ccusage/README.md +205 -0
- package/.devcontainer/features/ccusage/devcontainer-feature.json +38 -0
- package/.devcontainer/features/ccusage/install.sh +132 -0
- package/.devcontainer/features/claude-code/README.md +498 -0
- package/.devcontainer/features/claude-code/config/settings.json +36 -0
- package/.devcontainer/features/claude-code/config/system-prompt.md +118 -0
- package/.devcontainer/features/claude-code/config/world-building-sp.md +1432 -0
- package/.devcontainer/features/claude-code/devcontainer-feature.json +42 -0
- package/.devcontainer/features/claude-code/install.sh +466 -0
- package/.devcontainer/features/claude-monitor/README.md +74 -0
- package/.devcontainer/features/claude-monitor/devcontainer-feature.json +38 -0
- package/.devcontainer/features/claude-monitor/install.sh +99 -0
- package/.devcontainer/features/lsp-servers/README.md +85 -0
- package/.devcontainer/features/lsp-servers/devcontainer-feature.json +40 -0
- package/.devcontainer/features/lsp-servers/install.sh +116 -0
- package/.devcontainer/features/mcp-qdrant/CHANGES.md +399 -0
- package/.devcontainer/features/mcp-qdrant/README.md +474 -0
- package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +57 -0
- package/.devcontainer/features/mcp-qdrant/install.sh +295 -0
- package/.devcontainer/features/mcp-qdrant/poststart-hook.sh +129 -0
- package/.devcontainer/features/mcp-reasoner/README.md +177 -0
- package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +20 -0
- package/.devcontainer/features/mcp-reasoner/install.sh +177 -0
- package/.devcontainer/features/mcp-reasoner/poststart-hook.sh +67 -0
- package/.devcontainer/features/notify-hook/README.md +86 -0
- package/.devcontainer/features/notify-hook/devcontainer-feature.json +23 -0
- package/.devcontainer/features/notify-hook/install.sh +38 -0
- package/.devcontainer/features/splitrail/README.md +140 -0
- package/.devcontainer/features/splitrail/devcontainer-feature.json +34 -0
- package/.devcontainer/features/splitrail/install.sh +129 -0
- package/.devcontainer/features/tree-sitter/README.md +138 -0
- package/.devcontainer/features/tree-sitter/devcontainer-feature.json +52 -0
- package/.devcontainer/features/tree-sitter/install.sh +173 -0
- package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +106 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-file.py +101 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +137 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/SKILL.md +387 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/cli-flags-and-output.md +312 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/sdk-and-mcp.md +569 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/SKILL.md +309 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/compose-services.md +438 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/dockerfile-patterns.md +340 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/SKILL.md +412 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/container-lifecycle.md +388 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/resources-and-security.md +444 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/SKILL.md +344 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/middleware-and-lifespan.md +254 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/pydantic-models.md +245 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/routing-and-dependencies.md +255 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/sse-and-streaming.md +318 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/SKILL.md +345 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/agents-and-tools.md +271 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/models-and-streaming.md +422 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/SKILL.md +220 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/cross-vendor-principles.md +139 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/patterns-and-antipatterns.md +376 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/skill-authoring-patterns.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/SKILL.md +329 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/advanced-queries.md +314 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/javascript-patterns.md +323 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/python-patterns.md +354 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/schema-and-pragmas.md +326 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/SKILL.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/ai-sdk-svelte.md +128 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/component-patterns.md +332 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/layercake.md +203 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/migration-guide.md +350 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/runes-and-reactivity.md +328 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/spa-and-routing.md +262 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/svelte-dnd-action.md +181 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/SKILL.md +414 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/fastapi-testing.md +411 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/svelte-testing.md +538 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +110 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +108 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272create-pr.md +337 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272new.md +166 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272review-commit.md +290 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272work.md +257 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +184 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/.claude-plugin/plugin.json +6 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/config/planning-instructions.md +14 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/functional-conjuring-map.md +989 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/hooks/hooks.json +33 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/__pycache__/post-enhance-task.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py +71 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-plan.sh +68 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-task.sh +120 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-plan.py +133 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-task.py +253 -0
- package/.devcontainer/scripts/setup-aliases.sh +80 -0
- package/.devcontainer/scripts/setup-config.sh +28 -0
- package/.devcontainer/scripts/setup-irie-claude.sh +32 -0
- package/.devcontainer/scripts/setup-plugins.sh +80 -0
- package/.devcontainer/scripts/setup.sh +58 -0
- package/LICENSE.txt +674 -0
- package/README.md +267 -0
- package/package.json +44 -0
- package/setup.js +83 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Enhance plan generation and task management with custom post-processing",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "EnterPlanMode",
|
|
7
|
+
"hooks": [{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/enhance-planning.py",
|
|
10
|
+
"timeout": 5
|
|
11
|
+
}]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PostToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Write",
|
|
17
|
+
"hooks": [{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/post-enhance-plan.py",
|
|
20
|
+
"timeout": 30
|
|
21
|
+
}]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"matcher": "TaskCreate|TaskUpdate",
|
|
25
|
+
"hooks": [{
|
|
26
|
+
"type": "command",
|
|
27
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/post-enhance-task.py",
|
|
28
|
+
"timeout": 30
|
|
29
|
+
}]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
Binary file
|
package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Plan Enhancer PreToolUse Hook Script
|
|
4
|
+
|
|
5
|
+
Injects custom planning instructions when Claude enters plan mode.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_plugin_root() -> Path:
|
|
14
|
+
"""Get the plugin root directory."""
|
|
15
|
+
# Script is at ${CLAUDE_PLUGIN_ROOT}/scripts/enhance-planning.py
|
|
16
|
+
return Path(__file__).parent.parent
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_planning_instructions() -> str:
|
|
20
|
+
"""Load custom planning instructions from config file."""
|
|
21
|
+
config_path = get_plugin_root() / "config" / "planning-instructions.md"
|
|
22
|
+
|
|
23
|
+
if not config_path.exists():
|
|
24
|
+
return ""
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
return config_path.read_text().strip()
|
|
28
|
+
except Exception as e:
|
|
29
|
+
print(f"Warning: Could not read planning instructions: {e}", file=sys.stderr)
|
|
30
|
+
return ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def handle_enter_plan_mode(tool_input: dict) -> dict:
|
|
34
|
+
"""Handle EnterPlanMode tool - inject custom instructions."""
|
|
35
|
+
instructions = load_planning_instructions()
|
|
36
|
+
|
|
37
|
+
if not instructions:
|
|
38
|
+
# No instructions configured, allow without modification
|
|
39
|
+
return {"decision": "allow"}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
"decision": "allow",
|
|
43
|
+
"additionalContext": f"\n\n--- Custom Planning Instructions ---\n{instructions}\n--- End Custom Instructions ---\n",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main():
|
|
48
|
+
"""Main entry point."""
|
|
49
|
+
try:
|
|
50
|
+
# Read hook input from stdin
|
|
51
|
+
input_data = json.load(sys.stdin)
|
|
52
|
+
except json.JSONDecodeError as e:
|
|
53
|
+
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
tool_name = input_data.get("tool_name", "")
|
|
57
|
+
tool_input = input_data.get("tool_input", {})
|
|
58
|
+
|
|
59
|
+
# Route to appropriate handler
|
|
60
|
+
if tool_name == "EnterPlanMode":
|
|
61
|
+
result = handle_enter_plan_mode(tool_input)
|
|
62
|
+
else:
|
|
63
|
+
# Unknown tool, allow by default
|
|
64
|
+
result = {"decision": "allow"}
|
|
65
|
+
|
|
66
|
+
# Output result as JSON
|
|
67
|
+
print(json.dumps(result))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
main()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# Plan Enhancement Script
|
|
4
|
+
# =============================================================================
|
|
5
|
+
#
|
|
6
|
+
# This script is called after Claude writes a plan file.
|
|
7
|
+
# Customize it to enhance plans with your own logic.
|
|
8
|
+
#
|
|
9
|
+
# Usage: enhance-plan.sh <plan-file-path>
|
|
10
|
+
#
|
|
11
|
+
# The plan file content is already written. You can:
|
|
12
|
+
# - Read it, modify it, write it back in-place
|
|
13
|
+
# - Call external APIs (LLM critique, linters, validators)
|
|
14
|
+
# - Add sections, headers, or metadata
|
|
15
|
+
# - Validate structure and add missing elements
|
|
16
|
+
#
|
|
17
|
+
# Exit codes:
|
|
18
|
+
# 0 - Success (stdout is shown to Claude as enhancement output)
|
|
19
|
+
# Non-zero - Failure (stderr is shown as a warning)
|
|
20
|
+
#
|
|
21
|
+
# =============================================================================
|
|
22
|
+
|
|
23
|
+
PLAN_FILE="$1"
|
|
24
|
+
|
|
25
|
+
if [[ -z "$PLAN_FILE" ]]; then
|
|
26
|
+
echo "Error: No plan file path provided" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if [[ ! -f "$PLAN_FILE" ]]; then
|
|
31
|
+
echo "Error: Plan file not found: $PLAN_FILE" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# -----------------------------------------------------------------------------
|
|
36
|
+
# Example Enhancements (uncomment to enable)
|
|
37
|
+
# -----------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
# Example 1: Add timestamp header
|
|
40
|
+
# sed -i "1i<!-- Enhanced: $(date -Iseconds) -->" "$PLAN_FILE"
|
|
41
|
+
|
|
42
|
+
# Example 2: Add enhancement marker at end
|
|
43
|
+
# echo -e "\n---\n_Plan reviewed by enhancement script at $(date)_" >> "$PLAN_FILE"
|
|
44
|
+
|
|
45
|
+
# Example 3: Run through another LLM for critique
|
|
46
|
+
# CRITIQUE=$(cat "$PLAN_FILE" | llm "Briefly critique this plan in 2-3 bullet points")
|
|
47
|
+
# echo -e "\n## AI Critique\n$CRITIQUE" >> "$PLAN_FILE"
|
|
48
|
+
|
|
49
|
+
# Example 4: Validate required sections exist
|
|
50
|
+
# required_sections=("## Summary" "## Files to Modify" "## Testing Strategy")
|
|
51
|
+
# for section in "${required_sections[@]}"; do
|
|
52
|
+
# if ! grep -q "$section" "$PLAN_FILE"; then
|
|
53
|
+
# echo "Warning: Missing section: $section" >&2
|
|
54
|
+
# fi
|
|
55
|
+
# done
|
|
56
|
+
|
|
57
|
+
# Example 5: Add word/line count metadata
|
|
58
|
+
# LINES=$(wc -l < "$PLAN_FILE")
|
|
59
|
+
# WORDS=$(wc -w < "$PLAN_FILE")
|
|
60
|
+
# sed -i "1i<!-- Lines: $LINES | Words: $WORDS -->" "$PLAN_FILE"
|
|
61
|
+
|
|
62
|
+
# -----------------------------------------------------------------------------
|
|
63
|
+
# Your custom enhancement logic goes here
|
|
64
|
+
# -----------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
# Default: No enhancement, just pass through
|
|
67
|
+
echo "No custom enhancements configured"
|
|
68
|
+
exit 0
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# Task Enhancement Script
|
|
4
|
+
# =============================================================================
|
|
5
|
+
#
|
|
6
|
+
# This script is called after Claude creates or updates a task.
|
|
7
|
+
# Customize it to enhance tasks with your own logic.
|
|
8
|
+
#
|
|
9
|
+
# Usage: enhance-task.sh <task-json-file> <task-subject> [plan-path]
|
|
10
|
+
#
|
|
11
|
+
# Arguments:
|
|
12
|
+
# $1 - Path to the task JSON file
|
|
13
|
+
# $2 - Subject of the task being created/updated
|
|
14
|
+
# $3 - (Optional) Path to the session's plan file (if one exists)
|
|
15
|
+
#
|
|
16
|
+
# Environment variables:
|
|
17
|
+
# TASK_TOOL - Either "TaskCreate" or "TaskUpdate"
|
|
18
|
+
# PLAN_PATH - Path to the session's plan file (same as $3, for convenience)
|
|
19
|
+
#
|
|
20
|
+
# The task JSON file contains all tasks for this session as a JSON array.
|
|
21
|
+
# Each task object has: subject, description, status, activeForm, etc.
|
|
22
|
+
#
|
|
23
|
+
# Plan-Task Correlation:
|
|
24
|
+
# When a plan exists for this session, you can use $PLAN_PATH to read
|
|
25
|
+
# the enhanced plan content and use it to inform task enhancement.
|
|
26
|
+
# This allows tasks to reference or validate against the approved plan.
|
|
27
|
+
#
|
|
28
|
+
# You can:
|
|
29
|
+
# - Read/modify the JSON file in-place
|
|
30
|
+
# - Call external APIs (LLM critique, validators)
|
|
31
|
+
# - Add metadata or annotations to tasks
|
|
32
|
+
# - Validate task structure
|
|
33
|
+
# - Cross-reference tasks with the plan file
|
|
34
|
+
#
|
|
35
|
+
# Exit codes:
|
|
36
|
+
# 0 - Success (stdout is shown to Claude as enhancement output)
|
|
37
|
+
# Non-zero - Failure (stderr is shown as a warning)
|
|
38
|
+
#
|
|
39
|
+
# =============================================================================
|
|
40
|
+
|
|
41
|
+
TASK_FILE="$1"
|
|
42
|
+
TASK_SUBJECT="$2"
|
|
43
|
+
PLAN_PATH="${3:-${PLAN_PATH:-}}" # Use arg $3 or env var PLAN_PATH
|
|
44
|
+
TOOL="${TASK_TOOL:-unknown}"
|
|
45
|
+
|
|
46
|
+
if [[ -z "$TASK_FILE" ]]; then
|
|
47
|
+
echo "Error: No task file path provided" >&2
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if [[ ! -f "$TASK_FILE" ]]; then
|
|
52
|
+
echo "Error: Task file not found: $TASK_FILE" >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# -----------------------------------------------------------------------------
|
|
57
|
+
# Example Enhancements (uncomment to enable)
|
|
58
|
+
# -----------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
# Example 1: Add timestamp to the most recent task's description
|
|
61
|
+
# if command -v jq &> /dev/null; then
|
|
62
|
+
# TIMESTAMP=$(date -Iseconds)
|
|
63
|
+
# jq --arg ts "$TIMESTAMP" --arg subj "$TASK_SUBJECT" \
|
|
64
|
+
# '(.[] | select(.subject == $subj) | .description) += " [Enhanced: \($ts)]"' \
|
|
65
|
+
# "$TASK_FILE" > "${TASK_FILE}.tmp" && mv "${TASK_FILE}.tmp" "$TASK_FILE"
|
|
66
|
+
# fi
|
|
67
|
+
|
|
68
|
+
# Example 2: Log task creation/updates
|
|
69
|
+
# echo "[$(date -Iseconds)] $TOOL: $TASK_SUBJECT" >> /tmp/task-log.txt
|
|
70
|
+
|
|
71
|
+
# Example 3: Validate task has description
|
|
72
|
+
# if command -v jq &> /dev/null; then
|
|
73
|
+
# EMPTY_DESC=$(jq -r '.[] | select(.description == "" or .description == null) | .subject' "$TASK_FILE")
|
|
74
|
+
# if [[ -n "$EMPTY_DESC" ]]; then
|
|
75
|
+
# echo "Warning: Tasks with empty descriptions found" >&2
|
|
76
|
+
# fi
|
|
77
|
+
# fi
|
|
78
|
+
|
|
79
|
+
# Example 4: Add priority field based on keywords
|
|
80
|
+
# if command -v jq &> /dev/null; then
|
|
81
|
+
# jq '
|
|
82
|
+
# .[] |= (
|
|
83
|
+
# if (.subject | test("urgent|critical|ASAP"; "i")) then
|
|
84
|
+
# .priority = "high"
|
|
85
|
+
# elif (.subject | test("minor|low"; "i")) then
|
|
86
|
+
# .priority = "low"
|
|
87
|
+
# else
|
|
88
|
+
# .priority = "normal"
|
|
89
|
+
# end
|
|
90
|
+
# )
|
|
91
|
+
# ' "$TASK_FILE" > "${TASK_FILE}.tmp" && mv "${TASK_FILE}.tmp" "$TASK_FILE"
|
|
92
|
+
# fi
|
|
93
|
+
|
|
94
|
+
# Example 5: Export tasks to external system
|
|
95
|
+
# curl -X POST -H "Content-Type: application/json" \
|
|
96
|
+
# -d @"$TASK_FILE" \
|
|
97
|
+
# "https://your-api.example.com/tasks"
|
|
98
|
+
|
|
99
|
+
# Example 6: Use plan context for task enhancement (Plan-Task Correlation)
|
|
100
|
+
# If a plan file exists for this session, use it to enhance tasks
|
|
101
|
+
# if [[ -n "$PLAN_PATH" && -f "$PLAN_PATH" ]]; then
|
|
102
|
+
# # Extract plan title from the file
|
|
103
|
+
# PLAN_TITLE=$(head -1 "$PLAN_PATH" | sed 's/^# //')
|
|
104
|
+
# echo "Task created under plan: $PLAN_TITLE"
|
|
105
|
+
#
|
|
106
|
+
# # Example: Add plan reference to task metadata
|
|
107
|
+
# # if command -v jq &> /dev/null; then
|
|
108
|
+
# # jq --arg plan "$PLAN_PATH" --arg subj "$TASK_SUBJECT" \
|
|
109
|
+
# # '(.[] | select(.subject == $subj) | .metadata.planFile) = $plan' \
|
|
110
|
+
# # "$TASK_FILE" > "${TASK_FILE}.tmp" && mv "${TASK_FILE}.tmp" "$TASK_FILE"
|
|
111
|
+
# # fi
|
|
112
|
+
# fi
|
|
113
|
+
|
|
114
|
+
# -----------------------------------------------------------------------------
|
|
115
|
+
# Your custom enhancement logic goes here
|
|
116
|
+
# -----------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
# Default: No enhancement, just pass through
|
|
119
|
+
echo "No custom task enhancements configured"
|
|
120
|
+
exit 0
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Plan Enhancer PostToolUse Hook Script
|
|
4
|
+
|
|
5
|
+
Captures plan files after they're written, runs a user-customizable
|
|
6
|
+
enhancement script, and notifies Claude of the enhancement.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_plugin_root() -> Path:
|
|
16
|
+
"""Get the plugin root directory."""
|
|
17
|
+
# Script is at ${CLAUDE_PLUGIN_ROOT}/scripts/post-enhance-plan.py
|
|
18
|
+
return Path(__file__).parent.parent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_plan_file(file_path: str) -> bool:
|
|
22
|
+
"""Check if a file is a plan file based on path patterns."""
|
|
23
|
+
return "/plans/" in file_path or file_path.endswith(".plan.md")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_enhancement_script() -> Path | None:
|
|
27
|
+
"""Get the user's enhancement script if it exists and is executable."""
|
|
28
|
+
script_path = get_plugin_root() / "scripts" / "enhancers" / "enhance-plan.sh"
|
|
29
|
+
|
|
30
|
+
if script_path.exists() and script_path.is_file():
|
|
31
|
+
return script_path
|
|
32
|
+
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_enhancement_script(script_path: Path, plan_file_path: str) -> tuple[bool, str]:
|
|
37
|
+
"""
|
|
38
|
+
Run the enhancement script on the plan file.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
tuple: (success: bool, message: str)
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
[str(script_path), plan_file_path],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
timeout=25, # Leave buffer for hook timeout
|
|
49
|
+
cwd=get_plugin_root(),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if result.returncode == 0:
|
|
53
|
+
output = result.stdout.strip()
|
|
54
|
+
return True, output if output else "Enhancement completed successfully"
|
|
55
|
+
else:
|
|
56
|
+
stderr = result.stderr.strip()
|
|
57
|
+
return (
|
|
58
|
+
False,
|
|
59
|
+
f"Enhancement script failed (exit {result.returncode}): {stderr}",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
except subprocess.TimeoutExpired:
|
|
63
|
+
return False, "Enhancement script timed out"
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return False, f"Error running enhancement script: {e}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def handle_write(tool_input: dict, tool_response: dict) -> dict:
|
|
69
|
+
"""Handle Write tool PostToolUse - enhance plan files."""
|
|
70
|
+
file_path = tool_input.get("file_path", "")
|
|
71
|
+
|
|
72
|
+
# Only process plan files
|
|
73
|
+
if not is_plan_file(file_path):
|
|
74
|
+
return {} # Silent pass-through for non-plan files
|
|
75
|
+
|
|
76
|
+
# Check if enhancement script exists
|
|
77
|
+
script_path = get_enhancement_script()
|
|
78
|
+
if script_path is None:
|
|
79
|
+
return {} # No enhancement script configured
|
|
80
|
+
|
|
81
|
+
# Verify the file was written successfully
|
|
82
|
+
if not Path(file_path).exists():
|
|
83
|
+
return {} # File doesn't exist, skip enhancement
|
|
84
|
+
|
|
85
|
+
# Run the enhancement script
|
|
86
|
+
success, message = run_enhancement_script(script_path, file_path)
|
|
87
|
+
|
|
88
|
+
if success:
|
|
89
|
+
return {
|
|
90
|
+
"additionalContext": (
|
|
91
|
+
f"\n--- Plan Enhancement Applied ---\n"
|
|
92
|
+
f"The plan file at `{file_path}` was enhanced by a custom script.\n"
|
|
93
|
+
f"Output: {message}\n"
|
|
94
|
+
f"The file content has been modified. Re-read if you need the updated content.\n"
|
|
95
|
+
f"--- End Enhancement Notice ---\n"
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
else:
|
|
99
|
+
return {
|
|
100
|
+
"additionalContext": (
|
|
101
|
+
f"\n--- Plan Enhancement Warning ---\n"
|
|
102
|
+
f"Enhancement script ran but reported an issue:\n{message}\n"
|
|
103
|
+
f"--- End Enhancement Notice ---\n"
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main():
|
|
109
|
+
"""Main entry point."""
|
|
110
|
+
try:
|
|
111
|
+
# Read hook input from stdin
|
|
112
|
+
input_data = json.load(sys.stdin)
|
|
113
|
+
except json.JSONDecodeError as e:
|
|
114
|
+
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
|
115
|
+
sys.exit(1)
|
|
116
|
+
|
|
117
|
+
tool_name = input_data.get("tool_name", "")
|
|
118
|
+
tool_input = input_data.get("tool_input", {})
|
|
119
|
+
tool_response = input_data.get("tool_response", {})
|
|
120
|
+
|
|
121
|
+
# Route to appropriate handler
|
|
122
|
+
if tool_name == "Write":
|
|
123
|
+
result = handle_write(tool_input, tool_response)
|
|
124
|
+
else:
|
|
125
|
+
# Unknown tool, pass through silently
|
|
126
|
+
result = {}
|
|
127
|
+
|
|
128
|
+
# Output result as JSON
|
|
129
|
+
print(json.dumps(result))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
main()
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Task Enhancer PostToolUse Hook Script
|
|
4
|
+
|
|
5
|
+
Captures tasks after they're created/updated, runs a user-customizable
|
|
6
|
+
enhancement script, and notifies Claude of the enhancement.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_plugin_root() -> Path:
|
|
17
|
+
"""Get the plugin root directory."""
|
|
18
|
+
# Script is at ${CLAUDE_PLUGIN_ROOT}/scripts/post-enhance-task.py
|
|
19
|
+
return Path(__file__).parent.parent
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def find_session_plan(transcript_path: str, cwd: str) -> Path | None:
|
|
23
|
+
"""
|
|
24
|
+
Find the most recent plan file created in this session.
|
|
25
|
+
|
|
26
|
+
Uses two methods:
|
|
27
|
+
1. Read transcript for Write calls to /plans/ directory
|
|
28
|
+
2. Fallback: scan plans directory for most recently modified file
|
|
29
|
+
"""
|
|
30
|
+
# Method 1: Read transcript for Write calls to /plans/
|
|
31
|
+
if transcript_path:
|
|
32
|
+
transcript = Path(transcript_path)
|
|
33
|
+
if transcript.exists():
|
|
34
|
+
try:
|
|
35
|
+
with open(transcript) as f:
|
|
36
|
+
for line in f:
|
|
37
|
+
try:
|
|
38
|
+
entry = json.loads(line)
|
|
39
|
+
# Look for tool_use entries with Write tool
|
|
40
|
+
if (
|
|
41
|
+
entry.get("type") == "tool_use"
|
|
42
|
+
and entry.get("name") == "Write"
|
|
43
|
+
):
|
|
44
|
+
file_path = entry.get("input", {}).get("file_path", "")
|
|
45
|
+
if "/plans/" in file_path and Path(file_path).exists():
|
|
46
|
+
return Path(file_path)
|
|
47
|
+
except json.JSONDecodeError:
|
|
48
|
+
continue
|
|
49
|
+
except (OSError, IOError):
|
|
50
|
+
pass # Fall through to Method 2
|
|
51
|
+
|
|
52
|
+
# Method 2: Fallback - scan plans directory for most recent
|
|
53
|
+
plans_dir = Path(cwd) / ".claude" / "plans"
|
|
54
|
+
if plans_dir.exists():
|
|
55
|
+
plan_files = sorted(
|
|
56
|
+
plans_dir.glob("*.md"), key=lambda p: p.stat().st_mtime, reverse=True
|
|
57
|
+
)
|
|
58
|
+
if plan_files:
|
|
59
|
+
return plan_files[0]
|
|
60
|
+
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def find_task_file(session_id: str) -> Path | None:
|
|
65
|
+
"""
|
|
66
|
+
Find the task JSON file for the given session.
|
|
67
|
+
|
|
68
|
+
Task files are stored at:
|
|
69
|
+
- /workspaces/.claude/todos/{session_id}-agent-{session_id}.json
|
|
70
|
+
- ~/.claude/todos/{session_id}-agent-{session_id}.json
|
|
71
|
+
"""
|
|
72
|
+
if not session_id:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
# Build the expected filename pattern
|
|
76
|
+
filename = f"{session_id}-agent-{session_id}.json"
|
|
77
|
+
|
|
78
|
+
# Check possible locations
|
|
79
|
+
locations = [
|
|
80
|
+
Path("/workspaces/.claude/todos") / filename,
|
|
81
|
+
Path.home() / ".claude" / "todos" / filename,
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
for path in locations:
|
|
85
|
+
if path.exists():
|
|
86
|
+
return path
|
|
87
|
+
|
|
88
|
+
# Also try scanning the todos directories for any matching session
|
|
89
|
+
todos_dirs = [
|
|
90
|
+
Path("/workspaces/.claude/todos"),
|
|
91
|
+
Path.home() / ".claude" / "todos",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
for todos_dir in todos_dirs:
|
|
95
|
+
if todos_dir.exists():
|
|
96
|
+
# Look for files starting with the session_id
|
|
97
|
+
for f in todos_dir.glob(f"{session_id}*.json"):
|
|
98
|
+
return f
|
|
99
|
+
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_enhancement_script() -> Path | None:
|
|
104
|
+
"""Get the user's enhancement script if it exists."""
|
|
105
|
+
script_path = get_plugin_root() / "scripts" / "enhancers" / "enhance-task.sh"
|
|
106
|
+
|
|
107
|
+
if script_path.exists() and script_path.is_file():
|
|
108
|
+
return script_path
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def run_enhancement_script(
|
|
114
|
+
script_path: Path,
|
|
115
|
+
task_file_path: str,
|
|
116
|
+
task_subject: str,
|
|
117
|
+
tool_name: str,
|
|
118
|
+
plan_path: str | None = None,
|
|
119
|
+
) -> tuple[bool, str]:
|
|
120
|
+
"""
|
|
121
|
+
Run the enhancement script on the task file.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
script_path: Path to the enhancement script
|
|
125
|
+
task_file_path: Path to the task JSON file
|
|
126
|
+
task_subject: Subject of the task being created/updated
|
|
127
|
+
tool_name: Either "TaskCreate" or "TaskUpdate"
|
|
128
|
+
plan_path: Optional path to the session's plan file
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
tuple: (success: bool, message: str)
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
env = os.environ.copy()
|
|
135
|
+
env["TASK_TOOL"] = tool_name
|
|
136
|
+
if plan_path:
|
|
137
|
+
env["PLAN_PATH"] = plan_path
|
|
138
|
+
|
|
139
|
+
# Build arguments: script, task_file, subject, [plan_path]
|
|
140
|
+
args = [str(script_path), task_file_path, task_subject]
|
|
141
|
+
if plan_path:
|
|
142
|
+
args.append(plan_path)
|
|
143
|
+
|
|
144
|
+
result = subprocess.run(
|
|
145
|
+
args,
|
|
146
|
+
capture_output=True,
|
|
147
|
+
text=True,
|
|
148
|
+
timeout=25, # Leave buffer for hook timeout
|
|
149
|
+
cwd=get_plugin_root(),
|
|
150
|
+
env=env,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if result.returncode == 0:
|
|
154
|
+
output = result.stdout.strip()
|
|
155
|
+
return True, output if output else "Enhancement completed successfully"
|
|
156
|
+
else:
|
|
157
|
+
stderr = result.stderr.strip()
|
|
158
|
+
return (
|
|
159
|
+
False,
|
|
160
|
+
f"Enhancement script failed (exit {result.returncode}): {stderr}",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
except subprocess.TimeoutExpired:
|
|
164
|
+
return False, "Enhancement script timed out"
|
|
165
|
+
except Exception as e:
|
|
166
|
+
return False, f"Error running enhancement script: {e}"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def handle_task_tool(
|
|
170
|
+
tool_name: str,
|
|
171
|
+
tool_input: dict,
|
|
172
|
+
tool_response: dict,
|
|
173
|
+
session_id: str,
|
|
174
|
+
transcript_path: str = "",
|
|
175
|
+
cwd: str = "",
|
|
176
|
+
) -> dict:
|
|
177
|
+
"""Handle TaskCreate/TaskUpdate PostToolUse - enhance tasks."""
|
|
178
|
+
task_subject = tool_input.get("subject", "")
|
|
179
|
+
|
|
180
|
+
# For TaskUpdate, we might not have a subject in the input
|
|
181
|
+
if not task_subject and tool_name == "TaskUpdate":
|
|
182
|
+
task_subject = tool_input.get("taskId", "unknown")
|
|
183
|
+
|
|
184
|
+
# Check if enhancement script exists
|
|
185
|
+
script_path = get_enhancement_script()
|
|
186
|
+
if script_path is None:
|
|
187
|
+
return {} # No enhancement script configured
|
|
188
|
+
|
|
189
|
+
# Find the task file
|
|
190
|
+
task_file = find_task_file(session_id)
|
|
191
|
+
if task_file is None:
|
|
192
|
+
return {} # Task file not found, skip silently
|
|
193
|
+
|
|
194
|
+
# Find the session's plan file (if any)
|
|
195
|
+
plan_file = find_session_plan(transcript_path, cwd)
|
|
196
|
+
plan_path = str(plan_file) if plan_file else None
|
|
197
|
+
|
|
198
|
+
# Run the enhancement script
|
|
199
|
+
success, message = run_enhancement_script(
|
|
200
|
+
script_path, str(task_file), task_subject, tool_name, plan_path
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if success:
|
|
204
|
+
return {
|
|
205
|
+
"additionalContext": (
|
|
206
|
+
f"\n--- Task Enhancement Applied ---\n"
|
|
207
|
+
f"The task file at `{task_file}` was enhanced by a custom script.\n"
|
|
208
|
+
f"Tool: {tool_name} | Subject: {task_subject}\n"
|
|
209
|
+
f"Output: {message}\n"
|
|
210
|
+
f"--- End Enhancement Notice ---\n"
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
else:
|
|
214
|
+
return {
|
|
215
|
+
"additionalContext": (
|
|
216
|
+
f"\n--- Task Enhancement Warning ---\n"
|
|
217
|
+
f"Enhancement script ran but reported an issue:\n{message}\n"
|
|
218
|
+
f"--- End Enhancement Notice ---\n"
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def main():
|
|
224
|
+
"""Main entry point."""
|
|
225
|
+
try:
|
|
226
|
+
# Read hook input from stdin
|
|
227
|
+
input_data = json.load(sys.stdin)
|
|
228
|
+
except json.JSONDecodeError as e:
|
|
229
|
+
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
|
230
|
+
sys.exit(1)
|
|
231
|
+
|
|
232
|
+
tool_name = input_data.get("tool_name", "")
|
|
233
|
+
tool_input = input_data.get("tool_input", {})
|
|
234
|
+
tool_response = input_data.get("tool_response", {})
|
|
235
|
+
session_id = input_data.get("session_id", "")
|
|
236
|
+
transcript_path = input_data.get("transcript_path", "")
|
|
237
|
+
cwd = input_data.get("cwd", "")
|
|
238
|
+
|
|
239
|
+
# Route to handler for task tools
|
|
240
|
+
if tool_name in ("TaskCreate", "TaskUpdate"):
|
|
241
|
+
result = handle_task_tool(
|
|
242
|
+
tool_name, tool_input, tool_response, session_id, transcript_path, cwd
|
|
243
|
+
)
|
|
244
|
+
else:
|
|
245
|
+
# Unknown tool, pass through silently
|
|
246
|
+
result = {}
|
|
247
|
+
|
|
248
|
+
# Output result as JSON
|
|
249
|
+
print(json.dumps(result))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
main()
|