Compare commits
2 Commits
afc842e165
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ee34eb56d | ||
|
|
3a13161ec6 |
24
README.md
24
README.md
@@ -37,6 +37,26 @@ jrnl config set anthropic api_key YOUR_API_KEY
|
|||||||
jrnl config set-provider ollama
|
jrnl config set-provider ollama
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Configuration file: `~/.jrnl/config.json`
|
||||||
|
After installation, configure your LLM provider:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For Anthropic Claude
|
||||||
|
jrnl config set anthropic api_key YOUR_API_KEY
|
||||||
|
|
||||||
|
# For Ollama (local)
|
||||||
|
jrnl config set-provider ollama
|
||||||
|
|
||||||
|
# For llama.cpp (local)
|
||||||
|
jrnl config set-provider llamacpp
|
||||||
|
# Set llama.cpp binary path (if not default)
|
||||||
|
jrnl config set llamacpp llamacpp_bin /path/to/llamacpp/bin
|
||||||
|
# Set model directory (optional)
|
||||||
|
jrnl config set llamacpp model_dir ~/.llamacpp/models
|
||||||
|
# Set model name (optional)
|
||||||
|
jrnl config set llamacpp model_name Qwen/Qwen3-8B-GGUF
|
||||||
|
```
|
||||||
|
|
||||||
Configuration file: `~/.jrnl/config.json`
|
Configuration file: `~/.jrnl/config.json`
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -83,6 +103,10 @@ jrnl config
|
|||||||
|
|
||||||
# Switch LLM provider
|
# Switch LLM provider
|
||||||
jrnl config set-provider ollama
|
jrnl config set-provider ollama
|
||||||
|
|
||||||
|
# Switch LLM provider
|
||||||
|
jrnl config set-provider ollama
|
||||||
|
jrnl config set-provider llamacpp
|
||||||
|
|
||||||
# Set provider-specific settings
|
# Set provider-specific settings
|
||||||
jrnl config set anthropic model claude-3-5-sonnet-20241022
|
jrnl config set anthropic model claude-3-5-sonnet-20241022
|
||||||
|
|||||||
184
jrnl/cli.py
184
jrnl/cli.py
@@ -2,26 +2,26 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
from .commands import new, daily, logs, config_cmd, uninstall_cmd
|
from .commands import new, daily, logs, config_cmd, uninstall_cmd, regenerate
|
||||||
from .version import __version__
|
from .version import __version__
|
||||||
|
|
||||||
|
|
||||||
def create_parser():
|
def create_parser():
|
||||||
"""Create the argument parser with all subcommands."""
|
"""Create the argument parser with all subcommands."""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog='jrnl',
|
prog="jrnl",
|
||||||
description='Developer work journal for standups - automatically track your work with git hooks and LLM-powered summaries',
|
description="Developer work journal for standups - automatically track your work with git hooks and LLM-powered summaries",
|
||||||
epilog='Run "jrnl <command> --help" for more information on a specific command.'
|
epilog='Run "jrnl <command> --help" for more information on a specific command.',
|
||||||
)
|
)
|
||||||
parser.add_argument('--version', action='version', version=f'jrnl {__version__}')
|
parser.add_argument("--version", action="version", version=f"jrnl {__version__}")
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||||
|
|
||||||
# jrnl new
|
# jrnl new
|
||||||
new_parser = subparsers.add_parser(
|
new_parser = subparsers.add_parser(
|
||||||
'new',
|
"new",
|
||||||
help='Create a log entry',
|
help="Create a log entry",
|
||||||
epilog='''
|
epilog="""
|
||||||
Examples:
|
Examples:
|
||||||
# Create a manual log entry
|
# Create a manual log entry
|
||||||
jrnl new -m "fixed authentication bug"
|
jrnl new -m "fixed authentication bug"
|
||||||
@@ -33,22 +33,30 @@ Examples:
|
|||||||
for hash in $(git log -5 --format=%%H); do
|
for hash in $(git log -5 --format=%%H); do
|
||||||
jrnl new --git --repo-path "$(pwd)" --commit-hash "$hash"
|
jrnl new --git --repo-path "$(pwd)" --commit-hash "$hash"
|
||||||
done
|
done
|
||||||
''',
|
""",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
new_parser.add_argument("-m", "--message", help="Log message for manual entry")
|
||||||
|
new_parser.add_argument("-l", "--label", help="Optional label for this entry")
|
||||||
|
new_parser.add_argument(
|
||||||
|
"--git",
|
||||||
|
action="store_true",
|
||||||
|
help="Git mode: process a commit with LLM compression",
|
||||||
|
)
|
||||||
|
new_parser.add_argument("--repo-path", help="Repository path (required with --git)")
|
||||||
|
new_parser.add_argument(
|
||||||
|
"--commit-hash", help="Commit hash to process (required with --git)"
|
||||||
|
)
|
||||||
|
new_parser.add_argument(
|
||||||
|
"--debug", action="store_true", help="Enable debug output for troubleshooting"
|
||||||
)
|
)
|
||||||
new_parser.add_argument('-m', '--message', help='Log message for manual entry')
|
|
||||||
new_parser.add_argument('-l', '--label', help='Optional label for this entry')
|
|
||||||
new_parser.add_argument('--git', action='store_true',
|
|
||||||
help='Git mode: process a commit with LLM compression')
|
|
||||||
new_parser.add_argument('--repo-path', help='Repository path (required with --git)')
|
|
||||||
new_parser.add_argument('--commit-hash', help='Commit hash to process (required with --git)')
|
|
||||||
|
|
||||||
# jrnl daily / standup
|
# jrnl daily / standup
|
||||||
daily_parser = subparsers.add_parser(
|
daily_parser = subparsers.add_parser(
|
||||||
'daily',
|
"daily",
|
||||||
aliases=['standup'],
|
aliases=["standup"],
|
||||||
help='Generate standup message from your logs',
|
help="Generate standup message from your logs",
|
||||||
epilog='''
|
epilog="""
|
||||||
Examples:
|
Examples:
|
||||||
# Generate today's standup
|
# Generate today's standup
|
||||||
jrnl daily
|
jrnl daily
|
||||||
@@ -63,21 +71,33 @@ Examples:
|
|||||||
jrnl daily --delete 2024-12-12
|
jrnl daily --delete 2024-12-12
|
||||||
jrnl daily --delete today
|
jrnl daily --delete today
|
||||||
jrnl daily --delete latest
|
jrnl daily --delete latest
|
||||||
''',
|
""",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
daily_parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--days",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="Number of days to include (default: 1)",
|
||||||
|
)
|
||||||
|
daily_parser.add_argument(
|
||||||
|
"-r",
|
||||||
|
"--regenerate",
|
||||||
|
action="store_true",
|
||||||
|
help="Regenerate today's standup with latest logs",
|
||||||
|
)
|
||||||
|
daily_parser.add_argument(
|
||||||
|
"--delete",
|
||||||
|
metavar="DATE",
|
||||||
|
help='Delete a daily entry (DATE: YYYY-MM-DD, "today", or "latest")',
|
||||||
)
|
)
|
||||||
daily_parser.add_argument('-d', '--days', type=int, default=1,
|
|
||||||
help='Number of days to include (default: 1)')
|
|
||||||
daily_parser.add_argument('-r', '--regenerate', action='store_true',
|
|
||||||
help='Regenerate today\'s standup with latest logs')
|
|
||||||
daily_parser.add_argument('--delete', metavar='DATE',
|
|
||||||
help='Delete a daily entry (DATE: YYYY-MM-DD, "today", or "latest")')
|
|
||||||
|
|
||||||
# jrnl logs
|
# jrnl logs
|
||||||
logs_parser = subparsers.add_parser(
|
logs_parser = subparsers.add_parser(
|
||||||
'logs',
|
"logs",
|
||||||
help='View log entries',
|
help="View log entries",
|
||||||
epilog='''
|
epilog="""
|
||||||
Examples:
|
Examples:
|
||||||
# View last 50 logs (default)
|
# View last 50 logs (default)
|
||||||
jrnl logs
|
jrnl logs
|
||||||
@@ -90,21 +110,28 @@ Examples:
|
|||||||
|
|
||||||
# Delete a log entry by hash/label
|
# Delete a log entry by hash/label
|
||||||
jrnl logs --delete 5a546f30
|
jrnl logs --delete 5a546f30
|
||||||
''',
|
""",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
logs_parser.add_argument(
|
||||||
|
"-d", "--days", type=int, help="Filter logs by number of days"
|
||||||
|
)
|
||||||
|
logs_parser.add_argument(
|
||||||
|
"-n",
|
||||||
|
"--limit",
|
||||||
|
type=int,
|
||||||
|
default=50,
|
||||||
|
help="Maximum number of logs to show (default: 50)",
|
||||||
|
)
|
||||||
|
logs_parser.add_argument(
|
||||||
|
"--delete", metavar="HASH", help="Delete a log entry by its hash/label"
|
||||||
)
|
)
|
||||||
logs_parser.add_argument('-d', '--days', type=int,
|
|
||||||
help='Filter logs by number of days')
|
|
||||||
logs_parser.add_argument('-n', '--limit', type=int, default=50,
|
|
||||||
help='Maximum number of logs to show (default: 50)')
|
|
||||||
logs_parser.add_argument('--delete', metavar='HASH',
|
|
||||||
help='Delete a log entry by its hash/label')
|
|
||||||
|
|
||||||
# jrnl config
|
# jrnl config
|
||||||
config_parser = subparsers.add_parser(
|
config_parser = subparsers.add_parser(
|
||||||
'config',
|
"config",
|
||||||
help='Manage configuration',
|
help="Manage configuration",
|
||||||
epilog='''
|
epilog="""
|
||||||
Examples:
|
Examples:
|
||||||
# Show current configuration
|
# Show current configuration
|
||||||
jrnl config
|
jrnl config
|
||||||
@@ -123,18 +150,56 @@ Examples:
|
|||||||
|
|
||||||
# Re-enable a repository
|
# Re-enable a repository
|
||||||
jrnl config include /path/to/repo
|
jrnl config include /path/to/repo
|
||||||
''',
|
""",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
config_parser.add_argument(
|
||||||
|
"action",
|
||||||
|
nargs="?",
|
||||||
|
choices=[
|
||||||
|
"show",
|
||||||
|
"set-provider",
|
||||||
|
"set",
|
||||||
|
"exclude",
|
||||||
|
"include",
|
||||||
|
"exclude-current",
|
||||||
|
],
|
||||||
|
help="Configuration action",
|
||||||
|
)
|
||||||
|
config_parser.add_argument("args", nargs="*", help="Action arguments")
|
||||||
|
|
||||||
|
# jrnl regenerate
|
||||||
|
regenerate_parser = subparsers.add_parser(
|
||||||
|
"regenerate",
|
||||||
|
help="Regenerate commit compressions",
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
# Regenerate compressions for all commits from a repository
|
||||||
|
jrnl regenerate --repo-path /path/to/repo
|
||||||
|
|
||||||
|
# Regenerate without confirmation prompt
|
||||||
|
jrnl regenerate --repo-path /path/to/repo --yes
|
||||||
|
|
||||||
|
# Regenerate with debug output
|
||||||
|
jrnl regenerate --repo-path /path/to/repo --debug
|
||||||
|
""",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
regenerate_parser.add_argument(
|
||||||
|
"--repo-path", required=True, help="Repository path containing the commits"
|
||||||
|
)
|
||||||
|
regenerate_parser.add_argument(
|
||||||
|
"-y", "--yes", action="store_true", help="Skip confirmation prompt"
|
||||||
|
)
|
||||||
|
regenerate_parser.add_argument(
|
||||||
|
"--debug", action="store_true", help="Enable debug output"
|
||||||
)
|
)
|
||||||
config_parser.add_argument('action', nargs='?',
|
|
||||||
choices=['show', 'set-provider', 'set', 'exclude', 'include', 'exclude-current'],
|
|
||||||
help='Configuration action')
|
|
||||||
config_parser.add_argument('args', nargs='*', help='Action arguments')
|
|
||||||
|
|
||||||
# jrnl uninstall
|
# jrnl uninstall
|
||||||
uninstall_parser = subparsers.add_parser('uninstall', help='Uninstall jrnl')
|
uninstall_parser = subparsers.add_parser("uninstall", help="Uninstall jrnl")
|
||||||
uninstall_parser.add_argument('--no-backup', action='store_true',
|
uninstall_parser.add_argument(
|
||||||
help='Skip database backup')
|
"--no-backup", action="store_true", help="Skip database backup"
|
||||||
|
)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
@@ -150,15 +215,17 @@ def main():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Route to appropriate command handler
|
# Route to appropriate command handler
|
||||||
if args.command == 'new':
|
if args.command == "new":
|
||||||
return new.handle(args)
|
return new.handle(args)
|
||||||
elif args.command in ['daily', 'standup']:
|
elif args.command in ["daily", "standup"]:
|
||||||
return daily.handle(args)
|
return daily.handle(args)
|
||||||
elif args.command == 'logs':
|
elif args.command == "logs":
|
||||||
return logs.handle(args)
|
return logs.handle(args)
|
||||||
elif args.command == 'config':
|
elif args.command == "config":
|
||||||
return config_cmd.handle(args)
|
return config_cmd.handle(args)
|
||||||
elif args.command == 'uninstall':
|
elif args.command == "regenerate":
|
||||||
|
return regenerate.handle(args)
|
||||||
|
elif args.command == "uninstall":
|
||||||
return uninstall_cmd.handle(args)
|
return uninstall_cmd.handle(args)
|
||||||
else:
|
else:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
@@ -170,9 +237,10 @@ def main():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Unexpected error: {type(e).__name__}: {e}", file=sys.stderr)
|
print(f"Unexpected error: {type(e).__name__}: {e}", file=sys.stderr)
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
"""jrnl config command - Manage configuration."""
|
"""jrnl config command - Manage configuration."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
from ..utils.formatting import format_success
|
from ..utils.formatting import format_success
|
||||||
|
from ..utils.git_utils import get_repo_root
|
||||||
|
|
||||||
|
|
||||||
def handle(args):
|
def handle(args):
|
||||||
"""Handle the 'config' command."""
|
"""Handle the 'config' command."""
|
||||||
config = Config.load()
|
config = Config.load()
|
||||||
|
|
||||||
if not args.action or args.action == 'show':
|
if not args.action or args.action == "show":
|
||||||
return show_config(config)
|
return show_config(config)
|
||||||
elif args.action == 'set-provider':
|
elif args.action == "set-provider":
|
||||||
return set_provider(config, args)
|
return set_provider(config, args)
|
||||||
elif args.action == 'set':
|
elif args.action == "set":
|
||||||
return set_value(config, args)
|
return set_value(config, args)
|
||||||
elif args.action == 'exclude':
|
elif args.action == "exclude":
|
||||||
return exclude_repo(config, args)
|
return exclude_repo(config, args)
|
||||||
elif args.action == 'include':
|
elif args.action == "include":
|
||||||
return include_repo(config, args)
|
return include_repo(config, args)
|
||||||
elif args.action == 'exclude-current':
|
elif args.action == "exclude-current":
|
||||||
return exclude_current_repo(config)
|
return exclude_current_repo(config)
|
||||||
else:
|
else:
|
||||||
print(f"Unknown action: {args.action}")
|
print(f"Unknown action: {args.action}")
|
||||||
@@ -36,11 +36,11 @@ def show_config(config):
|
|||||||
|
|
||||||
# Show LLM provider settings
|
# Show LLM provider settings
|
||||||
print("\n LLM Providers:")
|
print("\n LLM Providers:")
|
||||||
providers = config.get('llm_providers', {})
|
providers = config.get("llm_providers", {})
|
||||||
for provider_name, provider_config in providers.items():
|
for provider_name, provider_config in providers.items():
|
||||||
print(f"\n {provider_name}:")
|
print(f"\n {provider_name}:")
|
||||||
for key, value in provider_config.items():
|
for key, value in provider_config.items():
|
||||||
if key == 'api_key' and value:
|
if key == "api_key" and value:
|
||||||
# Mask API key - show last 4 chars if long enough, otherwise full mask
|
# Mask API key - show last 4 chars if long enough, otherwise full mask
|
||||||
if len(value) > 4:
|
if len(value) > 4:
|
||||||
print(f" {key}: {'*' * 8}{value[-4:]}")
|
print(f" {key}: {'*' * 8}{value[-4:]}")
|
||||||
@@ -50,7 +50,7 @@ def show_config(config):
|
|||||||
print(f" {key}: {value}")
|
print(f" {key}: {value}")
|
||||||
|
|
||||||
# Show excluded repos
|
# Show excluded repos
|
||||||
excluded = config.get('excluded_repos', [])
|
excluded = config.get("excluded_repos", [])
|
||||||
print(f"\n Excluded Repositories: {len(excluded)}")
|
print(f"\n Excluded Repositories: {len(excluded)}")
|
||||||
for repo in excluded:
|
for repo in excluded:
|
||||||
print(f" - {repo}")
|
print(f" - {repo}")
|
||||||
@@ -65,12 +65,12 @@ def set_provider(config, args):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
provider = args.args[0]
|
provider = args.args[0]
|
||||||
if provider not in config.get('llm_providers', {}):
|
if provider not in config.get("llm_providers", {}):
|
||||||
print(f"Unknown provider: {provider}")
|
print(f"Unknown provider: {provider}")
|
||||||
print(f"Available: {', '.join(config.get('llm_providers', {}).keys())}")
|
print(f"Available: {', '.join(config.get('llm_providers', {}).keys())}")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
config['active_llm_provider'] = provider
|
config["active_llm_provider"] = provider
|
||||||
Config.save(config)
|
Config.save(config)
|
||||||
print(format_success(f"Active LLM provider set to {provider}"))
|
print(format_success(f"Active LLM provider set to {provider}"))
|
||||||
return 0
|
return 0
|
||||||
@@ -84,13 +84,14 @@ def set_value(config, args):
|
|||||||
|
|
||||||
provider, key, value = args.args
|
provider, key, value = args.args
|
||||||
|
|
||||||
if provider not in config.get('llm_providers', {}):
|
if provider not in config.get("llm_providers", {}):
|
||||||
print(f"Unknown provider: {provider}")
|
print(f"Unknown provider: {provider}")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Get allowed keys from default config
|
# Get allowed keys from default config
|
||||||
from ..config import Config as ConfigClass
|
from ..config import Config as ConfigClass
|
||||||
allowed_keys = ConfigClass.DEFAULT_CONFIG['llm_providers'].get(provider, {}).keys()
|
|
||||||
|
allowed_keys = ConfigClass.DEFAULT_CONFIG["llm_providers"].get(provider, {}).keys()
|
||||||
|
|
||||||
if key not in allowed_keys:
|
if key not in allowed_keys:
|
||||||
print(f"Unknown key '{key}' for provider '{provider}'")
|
print(f"Unknown key '{key}' for provider '{provider}'")
|
||||||
@@ -101,16 +102,16 @@ def set_value(config, args):
|
|||||||
if value.isdigit():
|
if value.isdigit():
|
||||||
value = int(value)
|
value = int(value)
|
||||||
|
|
||||||
config['llm_providers'][provider][key] = value
|
config["llm_providers"][provider][key] = value
|
||||||
Config.save(config)
|
Config.save(config)
|
||||||
|
|
||||||
# Mask API key in output
|
# Mask API key in output
|
||||||
display_value = value
|
display_value = value
|
||||||
if key == 'api_key' and value:
|
if key == "api_key" and value:
|
||||||
if len(str(value)) > 4:
|
if len(str(value)) > 4:
|
||||||
display_value = f"{'*' * 8}{str(value)[-4:]}"
|
display_value = f"{'*' * 8}{str(value)[-4:]}"
|
||||||
else:
|
else:
|
||||||
display_value = '*' * len(str(value))
|
display_value = "*" * len(str(value))
|
||||||
|
|
||||||
print(format_success(f"Set {provider}.{key} = {display_value}"))
|
print(format_success(f"Set {provider}.{key} = {display_value}"))
|
||||||
return 0
|
return 0
|
||||||
@@ -123,14 +124,14 @@ def exclude_repo(config, args):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
repo_path = os.path.abspath(args.args[0])
|
repo_path = os.path.abspath(args.args[0])
|
||||||
excluded = config.get('excluded_repos', [])
|
excluded = config.get("excluded_repos", [])
|
||||||
|
|
||||||
if repo_path in excluded:
|
if repo_path in excluded:
|
||||||
print(f"Repository already excluded: {repo_path}")
|
print(f"Repository already excluded: {repo_path}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
excluded.append(repo_path)
|
excluded.append(repo_path)
|
||||||
config['excluded_repos'] = excluded
|
config["excluded_repos"] = excluded
|
||||||
Config.save(config)
|
Config.save(config)
|
||||||
print(format_success(f"Excluded repository: {repo_path}"))
|
print(format_success(f"Excluded repository: {repo_path}"))
|
||||||
return 0
|
return 0
|
||||||
@@ -143,14 +144,14 @@ def include_repo(config, args):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
repo_path = os.path.abspath(args.args[0])
|
repo_path = os.path.abspath(args.args[0])
|
||||||
excluded = config.get('excluded_repos', [])
|
excluded = config.get("excluded_repos", [])
|
||||||
|
|
||||||
if repo_path not in excluded:
|
if repo_path not in excluded:
|
||||||
print(f"Repository not in exclude list: {repo_path}")
|
print(f"Repository not in exclude list: {repo_path}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
excluded.remove(repo_path)
|
excluded.remove(repo_path)
|
||||||
config['excluded_repos'] = excluded
|
config["excluded_repos"] = excluded
|
||||||
Config.save(config)
|
Config.save(config)
|
||||||
print(format_success(f"Re-enabled repository: {repo_path}"))
|
print(format_success(f"Re-enabled repository: {repo_path}"))
|
||||||
return 0
|
return 0
|
||||||
@@ -160,33 +161,29 @@ def exclude_current_repo(config):
|
|||||||
"""Exclude current repository."""
|
"""Exclude current repository."""
|
||||||
try:
|
try:
|
||||||
# Get current git repo path
|
# Get current git repo path
|
||||||
result = subprocess.run(
|
repo_path = get_repo_root()
|
||||||
['git', 'rev-parse', '--show-toplevel'],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True
|
|
||||||
)
|
|
||||||
repo_path = result.stdout.strip()
|
|
||||||
|
|
||||||
excluded = config.get('excluded_repos', [])
|
if repo_path is None:
|
||||||
|
print("Error: Not in a git repository")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
excluded = config.get("excluded_repos", [])
|
||||||
if repo_path in excluded:
|
if repo_path in excluded:
|
||||||
print(f"Current repository already excluded: {repo_path}")
|
print(f"Current repository already excluded: {repo_path}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
excluded.append(repo_path)
|
excluded.append(repo_path)
|
||||||
config['excluded_repos'] = excluded
|
config["excluded_repos"] = excluded
|
||||||
Config.save(config)
|
Config.save(config)
|
||||||
print(format_success(f"Excluded current repository: {repo_path}"))
|
print(format_success(f"Excluded current repository: {repo_path}"))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
print("Error: Not in a git repository")
|
|
||||||
return 1
|
|
||||||
except RuntimeError as e: # From Config class
|
except RuntimeError as e: # From Config class
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
return 1
|
return 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Unexpected error: {type(e).__name__}: {e}")
|
print(f"Unexpected error: {type(e).__name__}: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from ..database.operations import (
|
|||||||
get_previous_daily_before,
|
get_previous_daily_before,
|
||||||
get_daily_for_date,
|
get_daily_for_date,
|
||||||
insert_daily,
|
insert_daily,
|
||||||
delete_daily
|
delete_daily,
|
||||||
)
|
)
|
||||||
from ..database.models import Daily
|
from ..database.models import Daily
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
@@ -20,7 +20,7 @@ def handle(args):
|
|||||||
"""Handle the 'daily' command."""
|
"""Handle the 'daily' command."""
|
||||||
try:
|
try:
|
||||||
# Handle deletion if requested
|
# Handle deletion if requested
|
||||||
if hasattr(args, 'delete') and args.delete:
|
if hasattr(args, "delete") and args.delete:
|
||||||
return handle_delete(args.delete)
|
return handle_delete(args.delete)
|
||||||
|
|
||||||
config = Config.load()
|
config = Config.load()
|
||||||
@@ -29,7 +29,8 @@ def handle(args):
|
|||||||
if args.regenerate:
|
if args.regenerate:
|
||||||
cutoff = get_regenerate_cutoff()
|
cutoff = get_regenerate_cutoff()
|
||||||
else:
|
else:
|
||||||
cutoff = get_normal_cutoff()
|
# Use days parameter to determine cutoff
|
||||||
|
cutoff = get_datetime_ago(days=args.days)
|
||||||
|
|
||||||
# Get logs
|
# Get logs
|
||||||
logs = get_logs_since(cutoff)
|
logs = get_logs_since(cutoff)
|
||||||
@@ -49,16 +50,13 @@ def handle(args):
|
|||||||
# Generate daily message
|
# Generate daily message
|
||||||
print("Generating standup (this may take 10-30 seconds)...")
|
print("Generating standup (this may take 10-30 seconds)...")
|
||||||
daily_message = provider.generate_daily(
|
daily_message = provider.generate_daily(
|
||||||
logs=[log.to_dict() for log in logs],
|
logs=[log.to_dict() for log in logs], days=args.days
|
||||||
days=args.days
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save daily
|
# Save daily
|
||||||
today = get_current_date()
|
today = get_current_date()
|
||||||
daily = Daily(
|
daily = Daily(
|
||||||
timestamp=get_utc_now(),
|
timestamp=get_utc_now(), daily_date=today, daily_message=daily_message
|
||||||
daily_date=today,
|
|
||||||
daily_message=daily_message
|
|
||||||
)
|
)
|
||||||
|
|
||||||
insert_daily(daily)
|
insert_daily(daily)
|
||||||
@@ -66,7 +64,7 @@ def handle(args):
|
|||||||
# Display result
|
# Display result
|
||||||
print(format_daily_header(today))
|
print(format_daily_header(today))
|
||||||
print(daily_message)
|
print(daily_message)
|
||||||
print("\n" + "="*60 + "\n")
|
print("\n" + "=" * 60 + "\n")
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -79,6 +77,7 @@ def handle(args):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Unexpected error generating daily: {e}")
|
print(f"Unexpected error generating daily: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
@@ -87,9 +86,9 @@ def handle_delete(date_arg: str) -> int:
|
|||||||
"""Handle deleting a daily entry."""
|
"""Handle deleting a daily entry."""
|
||||||
try:
|
try:
|
||||||
# Resolve date argument
|
# Resolve date argument
|
||||||
if date_arg.lower() in ['today', 'latest']:
|
if date_arg.lower() in ["today", "latest"]:
|
||||||
# Get today's date or latest daily date
|
# Get today's date or latest daily date
|
||||||
if date_arg.lower() == 'today':
|
if date_arg.lower() == "today":
|
||||||
date = get_current_date()
|
date = get_current_date()
|
||||||
else:
|
else:
|
||||||
latest = get_latest_daily()
|
latest = get_latest_daily()
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"""jrnl new command - Create log entries."""
|
"""jrnl new command - Create log entries."""
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
import subprocess
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ..database.operations import insert_log
|
from ..database.operations import insert_log
|
||||||
from ..database.models import Log
|
from ..database.models import Log
|
||||||
from ..utils.date_utils import get_utc_now
|
from ..utils.date_utils import get_utc_now
|
||||||
from ..utils.formatting import format_success, format_error
|
from ..utils.formatting import format_success, format_error
|
||||||
|
from ..utils.git_utils import extract_commit_info
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
from ..llm_providers import get_provider
|
from ..llm_providers import get_provider
|
||||||
|
|
||||||
@@ -34,8 +34,8 @@ def handle_manual_mode(args):
|
|||||||
log = Log(
|
log = Log(
|
||||||
timestamp=get_utc_now(),
|
timestamp=get_utc_now(),
|
||||||
log_message=args.message,
|
log_message=args.message,
|
||||||
type='manual',
|
type="manual",
|
||||||
label=args.label or str(uuid.uuid4())[:8]
|
label=args.label or str(uuid.uuid4())[:8],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save to database
|
# Save to database
|
||||||
@@ -53,6 +53,7 @@ def handle_manual_mode(args):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(format_error(f"Unexpected error: {e}"))
|
print(format_error(f"Unexpected error: {e}"))
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
@@ -68,24 +69,35 @@ def handle_git_mode(args):
|
|||||||
# Extract commit info
|
# Extract commit info
|
||||||
commit_info = extract_commit_info(args.repo_path, args.commit_hash)
|
commit_info = extract_commit_info(args.repo_path, args.commit_hash)
|
||||||
if not commit_info:
|
if not commit_info:
|
||||||
|
if getattr(args, "debug", False):
|
||||||
|
print("[DEBUG] Failed to extract commit info.")
|
||||||
return 1 # Silently fail
|
return 1 # Silently fail
|
||||||
|
|
||||||
# Load config and get LLM provider
|
# Load config and get LLM provider
|
||||||
config = Config.load()
|
config = Config.load()
|
||||||
provider = get_provider(config)
|
provider = get_provider(config)
|
||||||
|
|
||||||
|
if getattr(args, "debug", False):
|
||||||
|
print(f"[DEBUG] Commit info: {commit_info}")
|
||||||
|
print(f"[DEBUG] Provider: {provider.__class__.__name__}")
|
||||||
|
print(f"[DEBUG] Provider config: {getattr(provider, 'config', {})}")
|
||||||
|
|
||||||
# Compress commit info
|
# Compress commit info
|
||||||
|
if hasattr(provider, "set_debug"):
|
||||||
|
provider.set_debug(True)
|
||||||
log_message = provider.compress_commit(
|
log_message = provider.compress_commit(
|
||||||
commit_message=commit_info['message'],
|
commit_message=commit_info["message"], commit_diff=commit_info["diff"]
|
||||||
commit_diff=commit_info['diff']
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if getattr(args, "debug", False):
|
||||||
|
print(f"[DEBUG] LLM output: {log_message}")
|
||||||
|
|
||||||
# Create log entry
|
# Create log entry
|
||||||
log = Log(
|
log = Log(
|
||||||
timestamp=get_utc_now(),
|
timestamp=get_utc_now(),
|
||||||
log_message=log_message,
|
log_message=log_message,
|
||||||
type='git-hook',
|
type="git-hook",
|
||||||
label=commit_info['hash'][:8]
|
label=commit_info["hash"][:8],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save to database
|
# Save to database
|
||||||
@@ -95,63 +107,26 @@ def handle_git_mode(args):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Log error but don't fail (never block commits)
|
# Log error but don't fail (never block commits)
|
||||||
|
if getattr(args, "debug", False):
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
print(f"[DEBUG] Exception: {type(e).__name__}: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
log_error(f"Error processing commit: {type(e).__name__}: {e}")
|
log_error(f"Error processing commit: {type(e).__name__}: {e}")
|
||||||
return 0 # Return success to avoid blocking commit
|
return 0 # Return success to avoid blocking commit
|
||||||
|
|
||||||
|
|
||||||
def extract_commit_info(repo_path: str, commit_hash: str) -> dict:
|
# extract_commit_info is now imported from utils.git_utils
|
||||||
"""Extract commit message and diff."""
|
|
||||||
try:
|
|
||||||
# Get commit message
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', '-C', repo_path, 'log', '-1', '--pretty=%B', commit_hash],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
commit_message = result.stdout.strip()
|
|
||||||
|
|
||||||
# Get commit diff with context
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', '-C', repo_path, 'show', '--unified=3', '--no-color', commit_hash],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
commit_diff = result.stdout
|
|
||||||
|
|
||||||
# Truncate large diffs to avoid exceeding LLM context limits
|
|
||||||
MAX_DIFF_SIZE = 50000 # characters
|
|
||||||
if len(commit_diff) > MAX_DIFF_SIZE:
|
|
||||||
commit_diff = commit_diff[:MAX_DIFF_SIZE] + "\n... (diff truncated for size)"
|
|
||||||
|
|
||||||
return {
|
|
||||||
'hash': commit_hash,
|
|
||||||
'message': commit_message,
|
|
||||||
'diff': commit_diff,
|
|
||||||
'repo': repo_path
|
|
||||||
}
|
|
||||||
except FileNotFoundError:
|
|
||||||
log_error("git command not found in PATH")
|
|
||||||
return None
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
log_error("Git command timed out")
|
|
||||||
return None
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
log_error(f"Git command failed: {e}")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
log_error(f"Unexpected error extracting commit: {type(e).__name__}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def log_error(message: str):
|
def log_error(message: str):
|
||||||
"""Log error to error log file."""
|
"""Log error to error log file."""
|
||||||
try:
|
try:
|
||||||
error_log = Path.home() / '.jrnl' / 'logs' / 'errors.log'
|
error_log = Path.home() / ".jrnl" / "logs" / "errors.log"
|
||||||
error_log.parent.mkdir(parents=True, exist_ok=True)
|
error_log.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(error_log, 'a') as f:
|
with open(error_log, "a") as f:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
f.write(f"{datetime.now().isoformat()} - {message}\n")
|
f.write(f"{datetime.now().isoformat()} - {message}\n")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # Silently fail logging
|
pass # Silently fail logging
|
||||||
|
|||||||
123
jrnl/commands/regenerate.py
Normal file
123
jrnl/commands/regenerate.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
"""jrnl regenerate command - Regenerate commit compressions."""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from ..database.operations import get_all_logs, get_connection
|
||||||
|
from ..config import Config
|
||||||
|
from ..llm_providers import get_provider
|
||||||
|
from ..utils.git_utils import extract_commit_info
|
||||||
|
from ..utils.formatting import format_error
|
||||||
|
|
||||||
|
|
||||||
|
def handle(args):
|
||||||
|
"""Handle the 'regenerate' command."""
|
||||||
|
try:
|
||||||
|
# Get all git-hook logs
|
||||||
|
all_logs = get_all_logs(limit=10000) # Get a large number
|
||||||
|
git_logs = [log for log in all_logs if log.type == "git-hook"]
|
||||||
|
|
||||||
|
if not git_logs:
|
||||||
|
print("No git commit logs found to regenerate.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"Found {len(git_logs)} git commit log(s).")
|
||||||
|
|
||||||
|
# Check if repo path is provided
|
||||||
|
if not args.repo_path:
|
||||||
|
print(
|
||||||
|
format_error(
|
||||||
|
"Repository path is required. Use: jrnl regenerate --repo-path /path/to/repo"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print("\nNote: The label field in logs contains commit hashes.")
|
||||||
|
print(" Provide the repo path where these commits exist.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Confirm with user
|
||||||
|
if not args.yes:
|
||||||
|
response = input(
|
||||||
|
f"\nRegenerate compressions for {len(git_logs)} log(s) from {args.repo_path}? (y/N): "
|
||||||
|
)
|
||||||
|
if response.lower() not in ["y", "yes"]:
|
||||||
|
print("Cancelled.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Load config and get LLM provider
|
||||||
|
config = Config.load()
|
||||||
|
provider = get_provider(config)
|
||||||
|
|
||||||
|
if args.debug and hasattr(provider, "set_debug"):
|
||||||
|
provider.set_debug(True)
|
||||||
|
|
||||||
|
# Process each log
|
||||||
|
success_count = 0
|
||||||
|
skip_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
for i, log in enumerate(git_logs, 1):
|
||||||
|
commit_hash = log.label
|
||||||
|
print(
|
||||||
|
f"\n[{i}/{len(git_logs)}] Processing commit {commit_hash}...", end=" "
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract commit info
|
||||||
|
commit_info = extract_commit_info(args.repo_path, commit_hash)
|
||||||
|
if not commit_info:
|
||||||
|
print("⊘ Skipped (commit not found in repo)")
|
||||||
|
skip_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Compress with LLM
|
||||||
|
try:
|
||||||
|
new_message = provider.compress_commit(
|
||||||
|
commit_message=commit_info["message"],
|
||||||
|
commit_diff=commit_info["diff"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update the log
|
||||||
|
if update_log_message(log.id, new_message):
|
||||||
|
print("✓ Updated")
|
||||||
|
success_count += 1
|
||||||
|
else:
|
||||||
|
print("❌ Failed to update database")
|
||||||
|
error_count += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
error_count += 1
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Regeneration complete:")
|
||||||
|
print(f" ✓ Successfully regenerated: {success_count}")
|
||||||
|
print(f" ⊘ Skipped: {skip_count}")
|
||||||
|
print(f" ❌ Errors: {error_count}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return 0 if error_count == 0 else 1
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(format_error(f"Database error: {e}"))
|
||||||
|
return 1
|
||||||
|
except RuntimeError as e: # From LLM providers or config
|
||||||
|
print(format_error(f"Error: {e}"))
|
||||||
|
return 1
|
||||||
|
except Exception as e:
|
||||||
|
print(format_error(f"Unexpected error: {e}"))
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def update_log_message(log_id: int, new_message: str) -> bool:
|
||||||
|
"""Update a log entry's message."""
|
||||||
|
try:
|
||||||
|
with get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""UPDATE logs SET log_message = ? WHERE id = ?""",
|
||||||
|
(new_message, log_id),
|
||||||
|
)
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
"""jrnl uninstall command - Uninstall the application."""
|
"""jrnl uninstall command - Uninstall the application."""
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from ..utils.git_utils import get_repo_root
|
||||||
|
|
||||||
|
|
||||||
def handle(args):
|
def handle(args):
|
||||||
@@ -12,24 +12,27 @@ def handle(args):
|
|||||||
|
|
||||||
# Confirm
|
# Confirm
|
||||||
response = input("\nContinue with uninstall? (y/N): ")
|
response = input("\nContinue with uninstall? (y/N): ")
|
||||||
if response.lower() not in ['y', 'yes']:
|
if response.lower() not in ["y", "yes"]:
|
||||||
print("Uninstall cancelled")
|
print("Uninstall cancelled")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# Find and run uninstall.sh
|
# Find and run uninstall.sh
|
||||||
# Assume it's in the git repo root
|
# Assume it's in the git repo root
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
repo_root_str = get_repo_root()
|
||||||
['git', 'rev-parse', '--show-toplevel'],
|
if repo_root_str is None:
|
||||||
capture_output=True,
|
print("Error: Could not locate uninstall script")
|
||||||
text=True,
|
print("Please run: ./uninstall.sh from the jrnl repository")
|
||||||
check=True
|
return 1
|
||||||
)
|
|
||||||
repo_root = Path(result.stdout.strip())
|
repo_root = Path(repo_root_str)
|
||||||
uninstall_script = repo_root / 'uninstall.sh'
|
uninstall_script = repo_root / "uninstall.sh"
|
||||||
|
|
||||||
if uninstall_script.exists():
|
if uninstall_script.exists():
|
||||||
subprocess.run(['bash', str(uninstall_script)], check=True)
|
# Read and execute the uninstall script using Python
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
subprocess.run(["bash", str(uninstall_script)], check=True)
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
print(f"Error: uninstall.sh not found at {uninstall_script}")
|
print(f"Error: uninstall.sh not found at {uninstall_script}")
|
||||||
@@ -37,6 +40,9 @@ def handle(args):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
print("Error: Could not locate uninstall script")
|
print("Error: Could not execute uninstall script")
|
||||||
print("Please run: ./uninstall.sh from the jrnl repository")
|
print("Please run: ./uninstall.sh from the jrnl repository")
|
||||||
return 1
|
return 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
return 1
|
||||||
|
|||||||
@@ -4,31 +4,41 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Configuration manager."""
|
"""Configuration manager."""
|
||||||
|
|
||||||
CONFIG_PATH = Path.home() / '.jrnl' / 'config.json'
|
CONFIG_PATH = Path.home() / ".jrnl" / "config.json"
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
'active_llm_provider': 'anthropic',
|
"active_llm_provider": "anthropic",
|
||||||
'llm_providers': {
|
"llm_providers": {
|
||||||
'anthropic': {
|
"anthropic": {
|
||||||
'api_key': '',
|
"api_key": "",
|
||||||
'model': 'claude-sonnet-4-5-20250929',
|
"model": "claude-sonnet-4-5-20250929",
|
||||||
'max_tokens_commit': 200,
|
"max_tokens_commit": 200,
|
||||||
'max_tokens_daily': 500
|
"max_tokens_daily": 500,
|
||||||
|
},
|
||||||
|
"ollama": {
|
||||||
|
"url": "http://localhost:11434",
|
||||||
|
"model": "llama3.1:8b",
|
||||||
|
"max_tokens_commit": 200,
|
||||||
|
"max_tokens_daily": 500,
|
||||||
|
},
|
||||||
|
"llamacpp": {
|
||||||
|
"model_dir": f"{str(Path.home())}/.llamacpp/models",
|
||||||
|
"model_name": "Qwen3-8B.Q4_K_M.gguf",
|
||||||
|
"max_tokens_commit": 200,
|
||||||
|
"max_tokens_daily": 500,
|
||||||
|
"n_ctx": 0,
|
||||||
|
"n_threads": None,
|
||||||
|
"strip_thinking": False,
|
||||||
},
|
},
|
||||||
'ollama': {
|
|
||||||
'url': 'http://localhost:11434',
|
|
||||||
'model': 'llama3.1:8b',
|
|
||||||
'max_tokens_commit': 200,
|
|
||||||
'max_tokens_daily': 500
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'git_hooks_enabled': True,
|
"git_hooks_enabled": True,
|
||||||
'excluded_repos': [],
|
"excluded_repos": [],
|
||||||
'standup_time': '10:30',
|
"standup_time": "10:30",
|
||||||
'timezone': 'local'
|
"timezone": "local",
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -38,7 +48,7 @@ class Config:
|
|||||||
return cls.DEFAULT_CONFIG.copy()
|
return cls.DEFAULT_CONFIG.copy()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(cls.CONFIG_PATH, 'r') as f:
|
with open(cls.CONFIG_PATH, "r") as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
|
|
||||||
# Merge with defaults (for new fields)
|
# Merge with defaults (for new fields)
|
||||||
@@ -51,12 +61,14 @@ class Config:
|
|||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
print(f"Config file is corrupted at line {e.lineno}: {e.msg}")
|
print(f"Config file is corrupted at line {e.lineno}: {e.msg}")
|
||||||
response = input("Recreate config with defaults? (y/N): ")
|
response = input("Recreate config with defaults? (y/N): ")
|
||||||
if response.lower() == 'y':
|
if response.lower() == "y":
|
||||||
cls.save(cls.DEFAULT_CONFIG.copy())
|
cls.save(cls.DEFAULT_CONFIG.copy())
|
||||||
return cls.DEFAULT_CONFIG.copy()
|
return cls.DEFAULT_CONFIG.copy()
|
||||||
raise RuntimeError(f"Invalid config file at {cls.CONFIG_PATH}")
|
raise RuntimeError(f"Invalid config file at {cls.CONFIG_PATH}")
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
raise RuntimeError(f"Cannot read config file (permission denied): {cls.CONFIG_PATH}")
|
raise RuntimeError(
|
||||||
|
f"Cannot read config file (permission denied): {cls.CONFIG_PATH}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"Failed to load config: {type(e).__name__}: {e}")
|
raise RuntimeError(f"Failed to load config: {type(e).__name__}: {e}")
|
||||||
|
|
||||||
@@ -65,14 +77,16 @@ class Config:
|
|||||||
"""Save configuration to file."""
|
"""Save configuration to file."""
|
||||||
try:
|
try:
|
||||||
cls.CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
cls.CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(cls.CONFIG_PATH, 'w') as f:
|
with open(cls.CONFIG_PATH, "w") as f:
|
||||||
json.dump(config, f, indent=2)
|
json.dump(config, f, indent=2)
|
||||||
|
|
||||||
# Set permissions to 600 for security (API keys)
|
# Set permissions to 600 for security (API keys)
|
||||||
cls.CONFIG_PATH.chmod(0o600)
|
cls.CONFIG_PATH.chmod(0o600)
|
||||||
|
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
raise RuntimeError(f"Cannot write config file (permission denied): {cls.CONFIG_PATH}")
|
raise RuntimeError(
|
||||||
|
f"Cannot write config file (permission denied): {cls.CONFIG_PATH}"
|
||||||
|
)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise RuntimeError(f"Failed to write config file: {e}")
|
raise RuntimeError(f"Failed to write config file: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -96,7 +110,11 @@ class Config:
|
|||||||
"""Deep merge two dictionaries."""
|
"""Deep merge two dictionaries."""
|
||||||
result = base.copy()
|
result = base.copy()
|
||||||
for key, value in updates.items():
|
for key, value in updates.items():
|
||||||
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
if (
|
||||||
|
key in result
|
||||||
|
and isinstance(result[key], dict)
|
||||||
|
and isinstance(value, dict)
|
||||||
|
):
|
||||||
result[key] = cls._deep_merge(result[key], value)
|
result[key] = cls._deep_merge(result[key], value)
|
||||||
else:
|
else:
|
||||||
result[key] = value
|
result[key] = value
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Git commit processing utilities."""
|
"""Git commit processing utilities."""
|
||||||
|
|
||||||
import subprocess
|
|
||||||
from typing import Optional, Dict
|
from typing import Optional, Dict
|
||||||
|
from ..utils.git_utils import extract_commit_info as _extract_commit_info
|
||||||
|
|
||||||
|
|
||||||
def extract_commit_info(repo_path: str, commit_hash: str) -> Optional[Dict]:
|
def extract_commit_info(repo_path: str, commit_hash: str) -> Optional[Dict]:
|
||||||
@@ -15,51 +15,12 @@ def extract_commit_info(repo_path: str, commit_hash: str) -> Optional[Dict]:
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary with commit info or None if extraction fails
|
Dictionary with commit info or None if extraction fails
|
||||||
"""
|
"""
|
||||||
try:
|
# Delegate to the git_utils implementation
|
||||||
# Get commit message
|
return _extract_commit_info(repo_path, commit_hash)
|
||||||
result = subprocess.run(
|
|
||||||
['git', '-C', repo_path, 'log', '-1', '--pretty=%B', commit_hash],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
commit_message = result.stdout.strip()
|
|
||||||
|
|
||||||
# Get commit diff with context
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', '-C', repo_path, 'show', '--unified=3', '--no-color', commit_hash],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
commit_diff = result.stdout
|
|
||||||
|
|
||||||
return {
|
|
||||||
'hash': commit_hash,
|
|
||||||
'message': commit_message,
|
|
||||||
'diff': commit_diff,
|
|
||||||
'repo': repo_path
|
|
||||||
}
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
# git not in PATH
|
|
||||||
return None
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return None
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
return None
|
|
||||||
except Exception:
|
|
||||||
# Catch-all for other subprocess errors
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_repo_name(repo_path: str) -> str:
|
def get_repo_name(repo_path: str) -> str:
|
||||||
"""Get repository name from path."""
|
"""Get repository name from path."""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
return Path(repo_path).name
|
return Path(repo_path).name
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
from .base import LLMProvider
|
from .base import LLMProvider
|
||||||
from .anthropic_provider import AnthropicProvider
|
from .anthropic_provider import AnthropicProvider
|
||||||
from .ollama_provider import OllamaProvider
|
from .ollama_provider import OllamaProvider
|
||||||
|
from .llamacpp_provider import LlamaCppProvider
|
||||||
|
|
||||||
PROVIDERS = {
|
PROVIDERS = {
|
||||||
'anthropic': AnthropicProvider,
|
'anthropic': AnthropicProvider,
|
||||||
'ollama': OllamaProvider,
|
'ollama': OllamaProvider,
|
||||||
|
'llamacpp': LlamaCppProvider,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -21,4 +23,4 @@ def get_provider(config: dict) -> LLMProvider:
|
|||||||
return PROVIDERS[provider_name](provider_config)
|
return PROVIDERS[provider_name](provider_config)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['LLMProvider', 'AnthropicProvider', 'OllamaProvider', 'get_provider']
|
__all__ = ['LLMProvider', 'AnthropicProvider', 'OllamaProvider', 'LlamaCppProvider', 'get_provider']
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Anthropic/Claude LLM provider."""
|
"""Anthropic/Claude LLM provider."""
|
||||||
|
|
||||||
# import anthropic
|
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
from .base import LLMProvider
|
from .base import LLMProvider
|
||||||
from .prompts import COMPRESS_COMMIT_PROMPT, GENERATE_DAILY_PROMPT
|
from .prompts import COMPRESS_COMMIT_PROMPT, GENERATE_DAILY_PROMPT
|
||||||
@@ -12,48 +11,40 @@ class AnthropicProvider(LLMProvider):
|
|||||||
|
|
||||||
def __init__(self, config: Dict):
|
def __init__(self, config: Dict):
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
self.api_key = config.get('api_key', '')
|
self.api_key = config.get("api_key", "")
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
raise ValueError("Anthropic API key not configured. Run: jrnl config set anthropic api_key YOUR_KEY")
|
raise ValueError(
|
||||||
|
"Anthropic API key not configured. Run: jrnl config set anthropic api_key YOUR_KEY"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.model = config.get("model", "claude-sonnet-4-5-20250929")
|
||||||
|
self.max_tokens_commit = config.get("max_tokens_commit", 200)
|
||||||
|
self.max_tokens_daily = config.get("max_tokens_daily", 500)
|
||||||
|
|
||||||
# self.client = anthropic.Anthropic(api_key=api_key)
|
|
||||||
self.model = config.get('model', 'claude-sonnet-4-5-20250929')
|
|
||||||
self.max_tokens_commit = config.get('max_tokens_commit', 200)
|
|
||||||
self.max_tokens_daily = config.get('max_tokens_daily', 500)
|
|
||||||
|
|
||||||
def _send_message(self, prompt: dict, max_tokens=200) -> dict:
|
def _send_message(self, prompt: dict, max_tokens=200) -> dict:
|
||||||
res = requests.post(
|
res = requests.post(
|
||||||
"https://api.anthropic.com/v1/messages",
|
"https://api.anthropic.com/v1/messages",
|
||||||
headers={
|
headers={
|
||||||
'x-api-key': self.api_key,
|
"x-api-key": self.api_key,
|
||||||
'anthropic-version': '2023-06-01',
|
"anthropic-version": "2023-06-01",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
json={
|
json={
|
||||||
'model': self.model,
|
"model": self.model,
|
||||||
'max_tokens': max_tokens,
|
"max_tokens": max_tokens,
|
||||||
'temperature': 0.3,
|
"temperature": 0.3,
|
||||||
'messages': [
|
"messages": [
|
||||||
{
|
{"role": "user", "content": [{"type": "text", "text": prompt}]}
|
||||||
'role': 'user',
|
],
|
||||||
'content': [
|
},
|
||||||
{
|
|
||||||
'type': 'text',
|
|
||||||
'text': prompt
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return res.json()
|
return res.json()
|
||||||
|
|
||||||
def compress_commit(self, commit_message: str, commit_diff: str) -> str:
|
def compress_commit(self, commit_message: str, commit_diff: str) -> str:
|
||||||
"""Compress commit using Claude."""
|
"""Compress commit using Claude."""
|
||||||
prompt = COMPRESS_COMMIT_PROMPT.format(
|
prompt = COMPRESS_COMMIT_PROMPT.format(
|
||||||
commit_message=commit_message,
|
commit_message=commit_message, commit_diff=commit_diff
|
||||||
commit_diff=commit_diff
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -61,8 +52,8 @@ class AnthropicProvider(LLMProvider):
|
|||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
max_tokens=self.max_tokens_commit,
|
max_tokens=self.max_tokens_commit,
|
||||||
)
|
)
|
||||||
|
|
||||||
return message.get('content', [])[0].get('text').strip()
|
return message.get("content", [])[0].get("text").strip()
|
||||||
except IndexError as e:
|
except IndexError as e:
|
||||||
return f"[Anthropic Error] {commit_message}"
|
return f"[Anthropic Error] {commit_message}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -71,26 +62,24 @@ class AnthropicProvider(LLMProvider):
|
|||||||
def generate_daily(self, logs: List[Dict], days: int = 1) -> str:
|
def generate_daily(self, logs: List[Dict], days: int = 1) -> str:
|
||||||
"""Generate daily standup using Claude."""
|
"""Generate daily standup using Claude."""
|
||||||
# Format logs for prompt
|
# Format logs for prompt
|
||||||
log_text = "\n".join([
|
log_text = "\n".join(
|
||||||
f"- [{log['type']}] {log['log_message']}"
|
[f"- [{log['type']}] {log['log_message']}" for log in logs]
|
||||||
for log in logs
|
|
||||||
])
|
|
||||||
|
|
||||||
prompt = GENERATE_DAILY_PROMPT.format(
|
|
||||||
days=days,
|
|
||||||
logs=log_text
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
prompt = GENERATE_DAILY_PROMPT.format(days=days, logs=log_text)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = self._send_message(
|
message = self._send_message(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
max_tokens=self.max_tokens_daily,
|
max_tokens=self.max_tokens_daily,
|
||||||
)
|
)
|
||||||
|
|
||||||
return message.get('content', [])[0].get('text').strip()
|
return message.get("content", [])[0].get("text").strip()
|
||||||
|
|
||||||
except IndexError as e:
|
except IndexError as e:
|
||||||
return f"[Anthropic Error] Failed to generate daily: {type(e).__name__}: {e}"
|
return (
|
||||||
|
f"[Anthropic Error] Failed to generate daily: {type(e).__name__}: {e}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"Failed to generate daily: {type(e).__name__}: {e}")
|
raise RuntimeError(f"Failed to generate daily: {type(e).__name__}: {e}")
|
||||||
|
|
||||||
@@ -100,7 +89,7 @@ class AnthropicProvider(LLMProvider):
|
|||||||
self.client.messages.create(
|
self.client.messages.create(
|
||||||
model=self.model,
|
model=self.model,
|
||||||
max_tokens=10,
|
max_tokens=10,
|
||||||
messages=[{"role": "user", "content": "test"}]
|
messages=[{"role": "user", "content": "test"}],
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
219
jrnl/llm_providers/llamacpp_provider.py
Normal file
219
jrnl/llm_providers/llamacpp_provider.py
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
"""llama.cpp local LLM provider."""
|
||||||
|
|
||||||
|
from typing import Dict, List, Optional, Any, Iterator, TYPE_CHECKING
|
||||||
|
from .base import LLMProvider
|
||||||
|
from .prompts import COMPRESS_COMMIT_PROMPT, GENERATE_DAILY_PROMPT
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from llama_cpp import Llama
|
||||||
|
|
||||||
|
LlamaType = Llama
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
from llama_cpp import Llama
|
||||||
|
|
||||||
|
LlamaType = Llama
|
||||||
|
except ImportError:
|
||||||
|
Llama = None # type: ignore
|
||||||
|
LlamaType = Any # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def suppress_stderr() -> Iterator[None]:
|
||||||
|
"""
|
||||||
|
Context manager to temporarily suppress stderr output.
|
||||||
|
|
||||||
|
Useful for silencing verbose library initialization messages.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
None
|
||||||
|
|
||||||
|
Example:
|
||||||
|
with suppress_stderr():
|
||||||
|
# Code that writes to stderr
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
original_stderr = sys.stderr
|
||||||
|
try:
|
||||||
|
sys.stderr = open(os.devnull, "w")
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
sys.stderr.close()
|
||||||
|
sys.stderr = original_stderr
|
||||||
|
|
||||||
|
|
||||||
|
class LlamaCppProvider(LLMProvider):
|
||||||
|
"""LLM provider using llama.cpp Python bindings."""
|
||||||
|
|
||||||
|
# Instance variable type hints
|
||||||
|
model_dir: str
|
||||||
|
model_name: str
|
||||||
|
model_path: str
|
||||||
|
max_tokens_commit: int
|
||||||
|
max_tokens_daily: int
|
||||||
|
n_ctx: int
|
||||||
|
n_threads: Optional[int]
|
||||||
|
_llama_instance: Optional[LlamaType]
|
||||||
|
_debug: bool
|
||||||
|
|
||||||
|
def test_connection(self) -> bool:
|
||||||
|
"""
|
||||||
|
Test if model is accessible and can be loaded.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if model file exists and can be initialized, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
model_exists = os.path.isfile(self._get_model_path())
|
||||||
|
if not model_exists:
|
||||||
|
return False
|
||||||
|
# Try to initialize the model
|
||||||
|
_ = self._get_llama_instance()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_debug(self, debug: bool) -> None:
|
||||||
|
"""Enable or disable debug mode."""
|
||||||
|
self._debug = debug
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
|
"""Initialize LlamaCpp provider with configuration."""
|
||||||
|
super().__init__(config)
|
||||||
|
# Model directory and default model
|
||||||
|
self.model_dir = config.get(
|
||||||
|
"model_dir", f"{os.path.expanduser('~')}/.llamacpp/models"
|
||||||
|
)
|
||||||
|
self.model_name = config.get("model_name", "Qwen3-8B.Q4_K_M.gguf")
|
||||||
|
self.model_path = config.get(
|
||||||
|
"model_path", f"{self.model_dir}/{self.model_name}"
|
||||||
|
)
|
||||||
|
self.max_tokens_commit = config.get("max_tokens_commit", 200)
|
||||||
|
self.max_tokens_daily = config.get("max_tokens_daily", 500)
|
||||||
|
self._llama_instance: Optional[LlamaType] = None
|
||||||
|
# n_ctx=0 uses the model's maximum context size from metadata
|
||||||
|
self.n_ctx = config.get("n_ctx", 0) # 0 = use model's max context
|
||||||
|
self.n_threads = config.get("n_threads", None) # CPU threads (None = auto)
|
||||||
|
self._debug = False
|
||||||
|
|
||||||
|
def _get_model_path(self) -> str:
|
||||||
|
"""Get the full path to the model file."""
|
||||||
|
return self.model_path
|
||||||
|
|
||||||
|
def _get_llama_instance(self) -> LlamaType:
|
||||||
|
"""
|
||||||
|
Get or create Llama instance (lazy loading).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Initialized Llama model instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If model file doesn't exist or fails to load
|
||||||
|
"""
|
||||||
|
if self._llama_instance is None:
|
||||||
|
debug_mode = getattr(self, "_debug", False)
|
||||||
|
if debug_mode:
|
||||||
|
print(f"[DEBUG] Loading model: {self._get_model_path()}")
|
||||||
|
ctx_msg = "model's maximum" if self.n_ctx == 0 else str(self.n_ctx)
|
||||||
|
print(f"[DEBUG] Context window: {ctx_msg}")
|
||||||
|
|
||||||
|
# Suppress stderr output unless in debug mode
|
||||||
|
if debug_mode:
|
||||||
|
self._llama_instance = Llama(
|
||||||
|
model_path=self._get_model_path(),
|
||||||
|
n_ctx=self.n_ctx,
|
||||||
|
n_threads=self.n_threads,
|
||||||
|
verbose=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
with suppress_stderr():
|
||||||
|
self._llama_instance = Llama(
|
||||||
|
model_path=self._get_model_path(),
|
||||||
|
n_ctx=self.n_ctx,
|
||||||
|
n_threads=self.n_threads,
|
||||||
|
verbose=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if debug_mode:
|
||||||
|
actual_ctx = self._llama_instance.n_ctx()
|
||||||
|
print(f"[DEBUG] Actual context size loaded: {actual_ctx}")
|
||||||
|
return self._llama_instance
|
||||||
|
|
||||||
|
def _run_llamacpp(self, prompt: str, max_tokens: int) -> str:
|
||||||
|
"""
|
||||||
|
Run inference using llama-cpp-python bindings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: The prompt to send to the model
|
||||||
|
max_tokens: Maximum number of tokens to generate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generated text response from the model
|
||||||
|
"""
|
||||||
|
if getattr(self, "_debug", False):
|
||||||
|
print(f"[DEBUG] llama.cpp prompt: {prompt}")
|
||||||
|
print(f"[DEBUG] max_tokens: {max_tokens}")
|
||||||
|
try:
|
||||||
|
llama = self._get_llama_instance()
|
||||||
|
output = llama.create_completion(
|
||||||
|
prompt,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
echo=False,
|
||||||
|
repeat_penalty=1,
|
||||||
|
temperature=1.0,
|
||||||
|
stop=["\n\n"],
|
||||||
|
)
|
||||||
|
if getattr(self, "_debug", False):
|
||||||
|
print(f"[DEBUG] llama.cpp output: {output}")
|
||||||
|
|
||||||
|
# Extract text from response
|
||||||
|
text: str
|
||||||
|
if isinstance(output, dict) and "choices" in output:
|
||||||
|
text = output["choices"][0]["text"]
|
||||||
|
elif isinstance(output, dict) and "text" in output:
|
||||||
|
text = output["text"]
|
||||||
|
else:
|
||||||
|
text = str(output)
|
||||||
|
|
||||||
|
return text.strip()
|
||||||
|
except Exception as e:
|
||||||
|
if getattr(self, "_debug", False):
|
||||||
|
print(f"[DEBUG] llama.cpp error: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
return f"llama.cpp error: {e}"
|
||||||
|
|
||||||
|
def compress_commit(self, commit_message: str, commit_diff: str) -> str:
|
||||||
|
"""
|
||||||
|
Compress a git commit into a concise summary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
commit_message: The original commit message
|
||||||
|
commit_diff: The commit diff content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Compressed commit summary
|
||||||
|
"""
|
||||||
|
prompt = COMPRESS_COMMIT_PROMPT.format(
|
||||||
|
commit_message=commit_message, commit_diff=commit_diff
|
||||||
|
)
|
||||||
|
return self._run_llamacpp(prompt, self.max_tokens_commit)
|
||||||
|
|
||||||
|
def generate_daily(self, logs: List[Dict[str, Any]], days: int = 1) -> str:
|
||||||
|
"""
|
||||||
|
Generate a daily standup summary from log entries.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
logs: List of log entry dictionaries
|
||||||
|
days: Number of days to include in the summary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generated daily standup summary
|
||||||
|
"""
|
||||||
|
prompt = GENERATE_DAILY_PROMPT.format(logs=json.dumps(logs), days=days)
|
||||||
|
return self._run_llamacpp(prompt, self.max_tokens_daily)
|
||||||
@@ -33,6 +33,6 @@ Create a compact paragraph (3-5 sentences) covering:
|
|||||||
2. What's planned next, if this is deductable from the information.
|
2. What's planned next, if this is deductable from the information.
|
||||||
3. Any obstacles or blockers (mention if none). Good indication of a blocker is commit message or todo-comments.
|
3. Any obstacles or blockers (mention if none). Good indication of a blocker is commit message or todo-comments.
|
||||||
|
|
||||||
Keep it professional but conversational. Use past tense for completed work.
|
Keep it professional but conversational. Use past tense for completed work. Do not give instructions to developer, just summarize the work like you would be participating the standup meeting yourself.
|
||||||
|
|
||||||
Your standup summary:"""
|
Your standup summary:"""
|
||||||
|
|||||||
74
jrnl/utils/git_utils.py
Normal file
74
jrnl/utils/git_utils.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"""Git utilities using GitPython library."""
|
||||||
|
|
||||||
|
from typing import Optional, Dict
|
||||||
|
import os
|
||||||
|
from git import Repo, exc as git_exc
|
||||||
|
|
||||||
|
|
||||||
|
def get_repo_root(path: Optional[str] = None) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get the root directory of a git repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to search from (defaults to current directory)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Absolute path to repository root, or None if not in a git repo
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if path:
|
||||||
|
repo = Repo(path, search_parent_directories=True)
|
||||||
|
else:
|
||||||
|
repo = Repo(os.getcwd(), search_parent_directories=True)
|
||||||
|
return repo.working_dir
|
||||||
|
except (git_exc.InvalidGitRepositoryError, git_exc.NoSuchPathError):
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extract_commit_info(repo_path: str, commit_hash: str) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Extract commit message and diff from a git repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo_path: Path to git repository
|
||||||
|
commit_hash: Commit hash to extract
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with commit info or None if extraction fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
repo = Repo(repo_path)
|
||||||
|
commit = repo.commit(commit_hash)
|
||||||
|
|
||||||
|
# Get commit message
|
||||||
|
commit_message = commit.message.strip()
|
||||||
|
|
||||||
|
# Get commit diff with context
|
||||||
|
# Get diff against parent (or empty tree for first commit)
|
||||||
|
if commit.parents:
|
||||||
|
diff_text = repo.git.show(commit_hash, unified=3, no_color=True)
|
||||||
|
else:
|
||||||
|
# First commit - diff against empty tree
|
||||||
|
diff_text = repo.git.show(commit_hash, unified=3, no_color=True)
|
||||||
|
|
||||||
|
# Truncate large diffs to avoid exceeding LLM context limits
|
||||||
|
MAX_DIFF_SIZE = 50000 # characters
|
||||||
|
if len(diff_text) > MAX_DIFF_SIZE:
|
||||||
|
diff_text = diff_text[:MAX_DIFF_SIZE] + "\n... (diff truncated for size)"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"hash": commit_hash,
|
||||||
|
"message": commit_message,
|
||||||
|
"diff": diff_text,
|
||||||
|
"repo": repo_path,
|
||||||
|
}
|
||||||
|
except (
|
||||||
|
git_exc.InvalidGitRepositoryError,
|
||||||
|
git_exc.BadName,
|
||||||
|
git_exc.GitCommandError,
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
anthropic>=0.39.0
|
|
||||||
requests>=2.31.0
|
requests>=2.31.0
|
||||||
|
llama-cpp-python>=0.2.0
|
||||||
|
GitPython>=3.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user