Merge pull request 'push-oynptmmzrplt' (#2) from push-oynptmmzrplt into main

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-03-11 22:47:24 +01:00
47 changed files with 4688 additions and 565 deletions

View File

@@ -0,0 +1,95 @@
{
"cachedGrowthBookFeatures": {
"tengu_claudeai_mcp_connectors": true,
"tengu_event_sampling_config": {},
"tengu_frond_boric": {},
"tengu_log_segment_events": false,
"tengu_log_datadog_events": true,
"tengu_marble_anvil": true,
"tengu_tool_pear": false,
"tengu_scarf_coffee": false,
"tengu_herring_clock": false,
"tengu_keybinding_customization_release": true,
"tengu_marble_whisper": true,
"tengu_amber_quartz": false,
"tengu_thinkback": false,
"tengu_cicada_nap_ms": 0,
"tengu_miraculo_the_bard": false,
"tengu_miraculo_the_bard2": false,
"tengu_paper_halyard": false,
"tengu_chomp_inflection": false,
"tengu_marble_sandcastle": true,
"tengu_desktop_upsell": {
"enable_shortcut_tip": false,
"enable_startup_dialog": false
},
"tengu_marble_lantern_disabled": false,
"tengu_pid_based_version_locking": true,
"tengu_mcp_elicitation": false,
"tengu_kairos_cron": true,
"tengu_pebble_leaf_prune": false,
"tengu_swinburne_dune": false,
"tengu_coral_fern": false,
"tengu_basalt_3kr": false,
"tengu_summarize_tool_results": true,
"tengu_bergotte_lantern": false,
"tengu_sotto_voce": true,
"tengu_system_prompt_global_cache": true,
"tengu_turtle_carbon": true,
"tengu_glacier_2xr": false,
"enhanced_telemetry_beta": false,
"tengu_streaming_tool_execution2": true,
"tengu_moth_copse": false,
"tengu_tool_search_unsupported_models": [
"haiku"
],
"tengu_defer_all_bn4": false,
"tengu_fgts": false,
"tengu_tst_hint_m7r": false,
"tengu_attribution_header": true,
"tengu_prompt_cache_1h_config": {
"allowlist": [
"repl_main_thread*",
"sdk",
"auto_mode"
]
},
"tengu_disable_streaming_to_non_streaming_fallback": false,
"tengu_tool_input_aliasing": false,
"tengu_satin_quoll": {},
"tengu_amber_prism": false,
"tengu_tight_weave": true,
"tengu_auto_background_agents": false
},
"firstStartTime": "2026-03-11T12:56:06.706Z",
"opusProMigrationComplete": true,
"sonnet1m45MigrationComplete": true,
"userID": "2525622ce171b408689c31f61f66ba3433629fa13b0656ed565073ba5f6dc2c6",
"clientDataCache": {
"data": {},
"timestamp": 1773233813304
},
"changelogLastFetched": 1773233777137,
"oauthAccount": {
"accountUuid": "f093e810-c7e5-4d6b-902b-ec1fa73cbc94",
"emailAddress": "eric.coissac@gmail.com",
"organizationUuid": "c5e61747-337f-4fed-bf22-ca9925e92650",
"hasExtraUsageEnabled": true,
"billingType": "stripe_subscription",
"accountCreatedAt": "2025-07-29T14:22:00.696404Z",
"subscriptionCreatedAt": "2025-10-02T16:14:05.694173Z",
"displayName": "Eric",
"organizationRole": "admin",
"workspaceRole": null,
"organizationName": "eric.coissac@gmail.com's Organization"
},
"claudeCodeFirstTokenDate": "2025-10-05T14:58:34.301962Z",
"penguinModeOrgEnabled": true,
"groveConfigCache": {
"f093e810-c7e5-4d6b-902b-ec1fa73cbc94": {
"grove_enabled": true,
"timestamp": 1773233813326
}
},
"cachedExtraUsageDisabledReason": "out_of_credits"
}

View File

@@ -0,0 +1 @@
{"claudeAiOauth":{"accessToken":"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","refreshToken":"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","expiresAt":1773262609887,"scopes":["user:inference","user:mcp_servers","user:profile","user:sessions:claude_code"],"subscriptionType":"pro","rateLimitTier":"default_claude_ai"}}

View File

@@ -0,0 +1,3 @@
{
"firstStartTime": "2026-03-11T12:56:06.706Z"
}

View File

@@ -0,0 +1,58 @@
{
"cachedGrowthBookFeatures": {
"tengu_claudeai_mcp_connectors": true,
"tengu_event_sampling_config": {},
"tengu_frond_boric": {},
"tengu_log_segment_events": false,
"tengu_log_datadog_events": true,
"tengu_marble_anvil": true,
"tengu_tool_pear": false,
"tengu_scarf_coffee": false,
"tengu_herring_clock": false,
"tengu_keybinding_customization_release": true,
"tengu_marble_whisper": true,
"tengu_amber_quartz": false,
"tengu_thinkback": false,
"tengu_cicada_nap_ms": 0,
"tengu_miraculo_the_bard": false,
"tengu_miraculo_the_bard2": false,
"tengu_paper_halyard": false,
"tengu_chomp_inflection": false,
"tengu_marble_sandcastle": true,
"tengu_desktop_upsell": {
"enable_shortcut_tip": false,
"enable_startup_dialog": false
},
"tengu_marble_lantern_disabled": false
},
"firstStartTime": "2026-03-11T12:56:06.706Z",
"opusProMigrationComplete": true,
"sonnet1m45MigrationComplete": true,
"userID": "2525622ce171b408689c31f61f66ba3433629fa13b0656ed565073ba5f6dc2c6",
"clientDataCache": {
"data": {},
"timestamp": 1773233813304
},
"changelogLastFetched": 1773233777137,
"oauthAccount": {
"accountUuid": "f093e810-c7e5-4d6b-902b-ec1fa73cbc94",
"emailAddress": "eric.coissac@gmail.com",
"organizationUuid": "c5e61747-337f-4fed-bf22-ca9925e92650",
"hasExtraUsageEnabled": true,
"billingType": "stripe_subscription",
"accountCreatedAt": "2025-07-29T14:22:00.696404Z",
"subscriptionCreatedAt": "2025-10-02T16:14:05.694173Z",
"displayName": "Eric",
"organizationRole": "admin",
"workspaceRole": null,
"organizationName": "eric.coissac@gmail.com's Organization"
},
"claudeCodeFirstTokenDate": "2025-10-05T14:58:34.301962Z",
"penguinModeOrgEnabled": true,
"groveConfigCache": {
"f093e810-c7e5-4d6b-902b-ec1fa73cbc94": {
"grove_enabled": true,
"timestamp": 1773233813326
}
}
}

View File

@@ -0,0 +1,59 @@
{
"cachedGrowthBookFeatures": {
"tengu_claudeai_mcp_connectors": true,
"tengu_event_sampling_config": {},
"tengu_frond_boric": {},
"tengu_log_segment_events": false,
"tengu_log_datadog_events": true,
"tengu_marble_anvil": true,
"tengu_tool_pear": false,
"tengu_scarf_coffee": false,
"tengu_herring_clock": false,
"tengu_keybinding_customization_release": true,
"tengu_marble_whisper": true,
"tengu_amber_quartz": false,
"tengu_thinkback": false,
"tengu_cicada_nap_ms": 0,
"tengu_miraculo_the_bard": false,
"tengu_miraculo_the_bard2": false,
"tengu_paper_halyard": false,
"tengu_chomp_inflection": false,
"tengu_marble_sandcastle": true,
"tengu_desktop_upsell": {
"enable_shortcut_tip": false,
"enable_startup_dialog": false
},
"tengu_marble_lantern_disabled": false,
"tengu_pid_based_version_locking": true
},
"firstStartTime": "2026-03-11T12:56:06.706Z",
"opusProMigrationComplete": true,
"sonnet1m45MigrationComplete": true,
"userID": "2525622ce171b408689c31f61f66ba3433629fa13b0656ed565073ba5f6dc2c6",
"clientDataCache": {
"data": {},
"timestamp": 1773233813304
},
"changelogLastFetched": 1773233777137,
"oauthAccount": {
"accountUuid": "f093e810-c7e5-4d6b-902b-ec1fa73cbc94",
"emailAddress": "eric.coissac@gmail.com",
"organizationUuid": "c5e61747-337f-4fed-bf22-ca9925e92650",
"hasExtraUsageEnabled": true,
"billingType": "stripe_subscription",
"accountCreatedAt": "2025-07-29T14:22:00.696404Z",
"subscriptionCreatedAt": "2025-10-02T16:14:05.694173Z",
"displayName": "Eric",
"organizationRole": "admin",
"workspaceRole": null,
"organizationName": "eric.coissac@gmail.com's Organization"
},
"claudeCodeFirstTokenDate": "2025-10-05T14:58:34.301962Z",
"penguinModeOrgEnabled": true,
"groveConfigCache": {
"f093e810-c7e5-4d6b-902b-ec1fa73cbc94": {
"grove_enabled": true,
"timestamp": 1773233813326
}
}
}

View File

@@ -0,0 +1,92 @@
{
"cachedGrowthBookFeatures": {
"tengu_claudeai_mcp_connectors": true,
"tengu_event_sampling_config": {},
"tengu_frond_boric": {},
"tengu_log_segment_events": false,
"tengu_log_datadog_events": true,
"tengu_marble_anvil": true,
"tengu_tool_pear": false,
"tengu_scarf_coffee": false,
"tengu_herring_clock": false,
"tengu_keybinding_customization_release": true,
"tengu_marble_whisper": true,
"tengu_amber_quartz": false,
"tengu_thinkback": false,
"tengu_cicada_nap_ms": 0,
"tengu_miraculo_the_bard": false,
"tengu_miraculo_the_bard2": false,
"tengu_paper_halyard": false,
"tengu_chomp_inflection": false,
"tengu_marble_sandcastle": true,
"tengu_desktop_upsell": {
"enable_shortcut_tip": false,
"enable_startup_dialog": false
},
"tengu_marble_lantern_disabled": false,
"tengu_pid_based_version_locking": true,
"tengu_mcp_elicitation": false,
"tengu_kairos_cron": true,
"tengu_pebble_leaf_prune": false,
"tengu_swinburne_dune": false,
"tengu_coral_fern": false,
"tengu_basalt_3kr": false,
"tengu_summarize_tool_results": true,
"tengu_bergotte_lantern": false,
"tengu_sotto_voce": true,
"tengu_system_prompt_global_cache": true,
"tengu_turtle_carbon": true,
"tengu_glacier_2xr": false,
"enhanced_telemetry_beta": false,
"tengu_streaming_tool_execution2": true,
"tengu_moth_copse": false,
"tengu_tool_search_unsupported_models": [
"haiku"
],
"tengu_defer_all_bn4": false,
"tengu_fgts": false,
"tengu_tst_hint_m7r": false,
"tengu_attribution_header": true,
"tengu_prompt_cache_1h_config": {
"allowlist": [
"repl_main_thread*",
"sdk",
"auto_mode"
]
},
"tengu_disable_streaming_to_non_streaming_fallback": false,
"tengu_tool_input_aliasing": false,
"tengu_satin_quoll": {}
},
"firstStartTime": "2026-03-11T12:56:06.706Z",
"opusProMigrationComplete": true,
"sonnet1m45MigrationComplete": true,
"userID": "2525622ce171b408689c31f61f66ba3433629fa13b0656ed565073ba5f6dc2c6",
"clientDataCache": {
"data": {},
"timestamp": 1773233813304
},
"changelogLastFetched": 1773233777137,
"oauthAccount": {
"accountUuid": "f093e810-c7e5-4d6b-902b-ec1fa73cbc94",
"emailAddress": "eric.coissac@gmail.com",
"organizationUuid": "c5e61747-337f-4fed-bf22-ca9925e92650",
"hasExtraUsageEnabled": true,
"billingType": "stripe_subscription",
"accountCreatedAt": "2025-07-29T14:22:00.696404Z",
"subscriptionCreatedAt": "2025-10-02T16:14:05.694173Z",
"displayName": "Eric",
"organizationRole": "admin",
"workspaceRole": null,
"organizationName": "eric.coissac@gmail.com's Organization"
},
"claudeCodeFirstTokenDate": "2025-10-05T14:58:34.301962Z",
"penguinModeOrgEnabled": true,
"groveConfigCache": {
"f093e810-c7e5-4d6b-902b-ec1fa73cbc94": {
"grove_enabled": true,
"timestamp": 1773233813326
}
},
"cachedExtraUsageDisabledReason": "out_of_credits"
}

View File

@@ -0,0 +1,93 @@
{
"cachedGrowthBookFeatures": {
"tengu_claudeai_mcp_connectors": true,
"tengu_event_sampling_config": {},
"tengu_frond_boric": {},
"tengu_log_segment_events": false,
"tengu_log_datadog_events": true,
"tengu_marble_anvil": true,
"tengu_tool_pear": false,
"tengu_scarf_coffee": false,
"tengu_herring_clock": false,
"tengu_keybinding_customization_release": true,
"tengu_marble_whisper": true,
"tengu_amber_quartz": false,
"tengu_thinkback": false,
"tengu_cicada_nap_ms": 0,
"tengu_miraculo_the_bard": false,
"tengu_miraculo_the_bard2": false,
"tengu_paper_halyard": false,
"tengu_chomp_inflection": false,
"tengu_marble_sandcastle": true,
"tengu_desktop_upsell": {
"enable_shortcut_tip": false,
"enable_startup_dialog": false
},
"tengu_marble_lantern_disabled": false,
"tengu_pid_based_version_locking": true,
"tengu_mcp_elicitation": false,
"tengu_kairos_cron": true,
"tengu_pebble_leaf_prune": false,
"tengu_swinburne_dune": false,
"tengu_coral_fern": false,
"tengu_basalt_3kr": false,
"tengu_summarize_tool_results": true,
"tengu_bergotte_lantern": false,
"tengu_sotto_voce": true,
"tengu_system_prompt_global_cache": true,
"tengu_turtle_carbon": true,
"tengu_glacier_2xr": false,
"enhanced_telemetry_beta": false,
"tengu_streaming_tool_execution2": true,
"tengu_moth_copse": false,
"tengu_tool_search_unsupported_models": [
"haiku"
],
"tengu_defer_all_bn4": false,
"tengu_fgts": false,
"tengu_tst_hint_m7r": false,
"tengu_attribution_header": true,
"tengu_prompt_cache_1h_config": {
"allowlist": [
"repl_main_thread*",
"sdk",
"auto_mode"
]
},
"tengu_disable_streaming_to_non_streaming_fallback": false,
"tengu_tool_input_aliasing": false,
"tengu_satin_quoll": {},
"tengu_amber_prism": false
},
"firstStartTime": "2026-03-11T12:56:06.706Z",
"opusProMigrationComplete": true,
"sonnet1m45MigrationComplete": true,
"userID": "2525622ce171b408689c31f61f66ba3433629fa13b0656ed565073ba5f6dc2c6",
"clientDataCache": {
"data": {},
"timestamp": 1773233813304
},
"changelogLastFetched": 1773233777137,
"oauthAccount": {
"accountUuid": "f093e810-c7e5-4d6b-902b-ec1fa73cbc94",
"emailAddress": "eric.coissac@gmail.com",
"organizationUuid": "c5e61747-337f-4fed-bf22-ca9925e92650",
"hasExtraUsageEnabled": true,
"billingType": "stripe_subscription",
"accountCreatedAt": "2025-07-29T14:22:00.696404Z",
"subscriptionCreatedAt": "2025-10-02T16:14:05.694173Z",
"displayName": "Eric",
"organizationRole": "admin",
"workspaceRole": null,
"organizationName": "eric.coissac@gmail.com's Organization"
},
"claudeCodeFirstTokenDate": "2025-10-05T14:58:34.301962Z",
"penguinModeOrgEnabled": true,
"groveConfigCache": {
"f093e810-c7e5-4d6b-902b-ec1fa73cbc94": {
"grove_enabled": true,
"timestamp": 1773233813326
}
},
"cachedExtraUsageDisabledReason": "out_of_credits"
}

2186
.claude-devcontainer/cache/changelog.md vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
{
"fetchedAt": "2026-03-11T12:56:07.264Z",
"plugins": [
{
"plugin": "code-review@claude-plugins-official",
"added_at": "2026-02-11T03:16:31.424Z",
"reason": "just-a-test",
"text": "This is a test #5"
},
{
"plugin": "fizz@testmkt-marketplace",
"added_at": "2026-02-12T00:00:00.000Z",
"reason": "security",
"text": "this is a security test"
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,19 @@
{"parentUuid":null,"isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","type":"user","message":{"role":"user","content":"Search for documentation about orla (github.com/dorcha-inc/orla) configuration, specifically:\n1. How to configure llm_backend for OpenAI-compatible API\n2. What config file format orla uses (YAML, TOML, JSON?)\n3. Where the config file should be placed (~/.orla, ~/.config/orla, orla.yaml, etc.)\n4. How to use a custom OpenAI-compatible endpoint with LM Studio\n\nLook at the source code at /usr/local/go/pkg/mod/github.com/dorcha-inc/ or any cached Go module. Also check if there are any orla config files in the system."},"uuid":"2f88d857-b27c-4a04-8199-33af4af15e10","timestamp":"2026-03-11T13:44:25.806Z"}
{"parentUuid":"2f88d857-b27c-4a04-8199-33af4af15e10","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01WRMXsZiGrk8P27RVrsVz5J","type":"message","role":"assistant","content":[{"type":"text","text":"I'll search for orla documentation and configuration information. Let me start by exploring the system for orla-related files and checking the Go modules."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":12529,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":12529,"ephemeral_1h_input_tokens":0},"output_tokens":5,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYwPjgdkNcUV5hmMaieH8","type":"assistant","uuid":"453a074b-95c7-40a7-92d9-75c1f60871b6","timestamp":"2026-03-11T13:44:27.428Z"}
{"parentUuid":"453a074b-95c7-40a7-92d9-75c1f60871b6","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01WRMXsZiGrk8P27RVrsVz5J","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01MFu86Ver8CeFKTXEJ3oRxP","name":"Bash","input":{"command":"find /usr/local/go/pkg/mod/github.com/dorcha-inc -type f -name \"*.md\" -o -name \"*.go\" -o -name \"*.yaml\" -o -name \"*.yml\" -o -name \"*.toml\" -o -name \"*.json\" 2>/dev/null | head -30"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":12529,"cache_read_input_tokens":0,"output_tokens":162,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":12529},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYwPjgdkNcUV5hmMaieH8","type":"assistant","uuid":"cbb729b2-0cda-4dd6-be31-a70566af412d","timestamp":"2026-03-11T13:44:27.776Z"}
{"parentUuid":"cbb729b2-0cda-4dd6-be31-a70566af412d","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Bash","command":"callback"},"parentToolUseID":"toolu_01MFu86Ver8CeFKTXEJ3oRxP","toolUseID":"toolu_01MFu86Ver8CeFKTXEJ3oRxP","timestamp":"2026-03-11T13:44:28.915Z","uuid":"7d01d136-c1d7-4bda-b83a-0dfbe3cdd8dd"}
{"parentUuid":"cbb729b2-0cda-4dd6-be31-a70566af412d","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01MFu86Ver8CeFKTXEJ3oRxP","type":"tool_result","content":"/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/.githooks/README.md\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/.github/FUNDING.yml\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/.github/workflows/build.yml\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/.github/workflows/release.yml\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/.gitleaks.toml\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/.golangci.yml\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/.goreleaser.yaml\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/.vscode/extensions.json\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/.vscode/settings.json\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/CHANGELOG.md\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/CODE_OF_CONDUCT.md\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/CONTRIBUTING.md\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/CONTRIBUTORS.md\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/MAINTAINERS.md\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/README.md\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/SECURITY.md\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla-test/README.md\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla-test/call_cmd.go\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla-test/init_cmd.go\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla-test/main.go\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla-test/validate.go\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla/agent_cmd.go\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla/cache_cmd.go\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla/daemon_cmd.go\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla/main.go\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla/main_test.go\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla/serve_cmd.go\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla/tool_cmd.go\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla/tool_info_cmd.go\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/cmd/orla/tool_install_cmd.go","is_error":false}]},"uuid":"3595f29e-b29e-47b5-9038-fb279612d521","timestamp":"2026-03-11T13:44:28.915Z","sourceToolAssistantUUID":"cbb729b2-0cda-4dd6-be31-a70566af412d"}
{"parentUuid":"3595f29e-b29e-47b5-9038-fb279612d521","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01MCbhjbsfVEBevEHCJiDfTX","type":"message","role":"assistant","content":[{"type":"text","text":"Let me search for README and configuration files more specifically:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":1368,"cache_read_input_tokens":12529,"cache_creation":{"ephemeral_5m_input_tokens":1368,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYwPjuSgNzwbFmNS5c1wn","type":"assistant","uuid":"2e05d35b-a758-4dad-8f40-18c7bd896100","timestamp":"2026-03-11T13:44:29.770Z"}
{"parentUuid":"2e05d35b-a758-4dad-8f40-18c7bd896100","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01MCbhjbsfVEBevEHCJiDfTX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014z1XCyo2WK9cACqxC5kMoE","name":"Read","input":{"file_path":"/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/README.md"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":1368,"cache_read_input_tokens":12529,"cache_creation":{"ephemeral_5m_input_tokens":1368,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYwPjuSgNzwbFmNS5c1wn","type":"assistant","uuid":"d655eebd-77b7-4463-b9bf-e94dd7f4d93a","timestamp":"2026-03-11T13:44:30.278Z"}
{"parentUuid":"d655eebd-77b7-4463-b9bf-e94dd7f4d93a","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"Tool permission request failed: Error: Tool use aborted","is_error":true,"tool_use_id":"toolu_014z1XCyo2WK9cACqxC5kMoE"}]},"uuid":"71cbb790-8980-4dbe-874e-8b4278bc8761","timestamp":"2026-03-11T13:44:30.339Z","toolUseResult":"Error: Tool permission request failed: Error: Tool use aborted","sourceToolAssistantUUID":"d655eebd-77b7-4463-b9bf-e94dd7f4d93a"}
{"parentUuid":"71cbb790-8980-4dbe-874e-8b4278bc8761","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01MCbhjbsfVEBevEHCJiDfTX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01MkzKb5UxgTwtSWVYk1fY6A","name":"Bash","input":{"command":"find /usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1 -type f \\( -name \"*.md\" -o -name \"config*\" \\) | grep -i config"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":1368,"cache_read_input_tokens":12529,"cache_creation":{"ephemeral_5m_input_tokens":1368,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYwPjuSgNzwbFmNS5c1wn","type":"assistant","uuid":"95a62859-af65-409b-945f-afe0cc9859f9","timestamp":"2026-03-11T13:44:31.180Z"}
{"parentUuid":"95a62859-af65-409b-945f-afe0cc9859f9","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"<tool_use_error>Cancelled: parallel tool call Read(/usr/local/go/pkg/mod/github.com/dorcha-…) errored</tool_use_error>","is_error":true,"tool_use_id":"toolu_01MkzKb5UxgTwtSWVYk1fY6A"}]},"uuid":"1cb9f541-7610-4371-8465-6a5e9f318ef4","timestamp":"2026-03-11T13:44:31.186Z","toolUseResult":"Cancelled: parallel tool call Read(/usr/local/go/pkg/mod/github.com/dorcha-…) errored","sourceToolAssistantUUID":"95a62859-af65-409b-945f-afe0cc9859f9"}
{"parentUuid":"1cb9f541-7610-4371-8465-6a5e9f318ef4","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01MCbhjbsfVEBevEHCJiDfTX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01E5HuDmbXkhQ76W7LmjsHoZ","name":"Bash","input":{"command":"find /usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1 -type f -name \"*.go\" | xargs grep -l \"llm_backend\\|config\" | head -10"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":1368,"cache_read_input_tokens":12529,"output_tokens":281,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":1368},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYwPjuSgNzwbFmNS5c1wn","type":"assistant","uuid":"186d25b8-2bac-41d7-9c44-ae85f9532513","timestamp":"2026-03-11T13:44:31.842Z"}
{"parentUuid":"186d25b8-2bac-41d7-9c44-ae85f9532513","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"<tool_use_error>Cancelled: parallel tool call Read(/usr/local/go/pkg/mod/github.com/dorcha-…) errored</tool_use_error>","is_error":true,"tool_use_id":"toolu_01E5HuDmbXkhQ76W7LmjsHoZ"}]},"uuid":"4101ff98-02cd-498f-bb79-d2036b317c8a","timestamp":"2026-03-11T13:44:31.848Z","toolUseResult":"Cancelled: parallel tool call Read(/usr/local/go/pkg/mod/github.com/dorcha-…) errored","sourceToolAssistantUUID":"186d25b8-2bac-41d7-9c44-ae85f9532513"}
{"parentUuid":"4101ff98-02cd-498f-bb79-d2036b317c8a","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01PAGPw45scRgRLysCoMGxr2","type":"message","role":"assistant","content":[{"type":"text","text":"Let me try a different approach:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":493,"cache_read_input_tokens":13897,"cache_creation":{"ephemeral_5m_input_tokens":493,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYwPk7s3a5xGfh79ZPxqE","type":"assistant","uuid":"9ea694cd-a2a3-498c-a878-baa49c58000c","timestamp":"2026-03-11T13:44:32.688Z"}
{"parentUuid":"9ea694cd-a2a3-498c-a878-baa49c58000c","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01PAGPw45scRgRLysCoMGxr2","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XqVdyDYPWtWbH29c9Pmtcz","name":"Bash","input":{"command":"ls -la /usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":493,"cache_read_input_tokens":13897,"cache_creation":{"ephemeral_5m_input_tokens":493,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYwPk7s3a5xGfh79ZPxqE","type":"assistant","uuid":"bbb0b718-7ea9-46f5-9740-7c1c6722381d","timestamp":"2026-03-11T13:44:33.023Z"}
{"parentUuid":"bbb0b718-7ea9-46f5-9740-7c1c6722381d","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01PAGPw45scRgRLysCoMGxr2","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016nzp7WpZDqLwFYi4Z84Z3P","name":"Bash","input":{"command":"grep -r \"llm_backend\" /usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1 2>/dev/null | head -20"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":493,"cache_read_input_tokens":13897,"output_tokens":174,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":493},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYwPk7s3a5xGfh79ZPxqE","type":"assistant","uuid":"ec4cdee6-b0aa-4e89-a458-d70299f1c358","timestamp":"2026-03-11T13:44:33.250Z"}
{"parentUuid":"ec4cdee6-b0aa-4e89-a458-d70299f1c358","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Bash","command":"callback"},"parentToolUseID":"toolu_016nzp7WpZDqLwFYi4Z84Z3P","toolUseID":"toolu_016nzp7WpZDqLwFYi4Z84Z3P","timestamp":"2026-03-11T13:44:36.191Z","uuid":"ed1cd086-1858-42e6-a33c-6695935a9e29"}
{"parentUuid":"ec4cdee6-b0aa-4e89-a458-d70299f1c358","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_016nzp7WpZDqLwFYi4Z84Z3P","type":"tool_result","content":"/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/.github/workflows/release.yml: - name: Test with llm_backend configuration\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/.github/workflows/release.yml: - name: Test with llm_backend configuration\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/README.md:After installation, configure Orla to use your remote Ollama server by setting either the `OLLAMA_HOST` or the `ORLA_OLLAMA_HOST` environment variable, or using the `llm_backend` configuration in your `orla.yaml`:\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/README.md:llm_backend:\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/config/config.go:\tLLMBackend *core.LLMBackend `yaml:\"llm_backend,omitempty\" mapstructure:\"llm_backend\"` // LLM backend configuration (endpoint, type, api_key)\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/ollama.go:// 1. llm_backend.endpoint config (if llm_backend.type is \"ollama\")\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/ollama.go:\t// Check llm_backend config first\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/ollama.go:\t\t\treturn \"\", fmt.Errorf(\"llm_backend.endpoint is required if llm_backend is set\")\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/ollama.go:\t\t\treturn \"\", fmt.Errorf(\"llm_backend.type is required if llm_backend is set\")\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/ollama.go:\t\t\treturn \"\", fmt.Errorf(\"[BUG] llm_backend.type must be %s, got '%s': we should not be using this function for non-ollama inference servers\", core.LLMInferenceAPITypeOllama, cfg.LLMBackend.Type)\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/ollama_integration_test.go:// using llm_backend configuration. This test uses testcontainers to spin up an Ollama container\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/ollama_integration_test.go:\t// Test with llm_backend configuration pointing to the container\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/ollama_test.go:// Tests for getOllamaEndpoint with llm_backend configuration\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/ollama_test.go:\tassert.Contains(t, err.Error(), \"llm_backend.endpoint is required\")\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/ollama_test.go:\tassert.Contains(t, err.Error(), \"llm_backend.type is required\")\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/ollama_test.go:\tassert.Contains(t, err.Error(), \"llm_backend.type must be\")\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/openai.go:\t\treturn \"\", \"\", fmt.Errorf(\"llm_backend configuration is required for OpenAI-compatible API\")\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/openai.go:\t\treturn \"\", \"\", fmt.Errorf(\"llm_backend.endpoint is required\")\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/openai.go:\t\treturn \"\", \"\", fmt.Errorf(\"llm_backend.type is required if llm_backend is set\")\n/usr/local/go/pkg/mod/github.com/dorcha-inc/orla@v1.1.1/internal/model/openai.go:\t\treturn \"\", \"\", fmt.Errorf(\"[BUG] llm_backend.type must be %s, got '%s': we should not be using this function for non-openai inference servers\", core.LLMInferenceAPITypeOpenAI, cfg.LLMBackend.Type)","is_error":false}]},"uuid":"5992985f-2797-46aa-b0de-b01c601b9412","timestamp":"2026-03-11T13:44:36.191Z","sourceToolAssistantUUID":"ec4cdee6-b0aa-4e89-a458-d70299f1c358"}
{"parentUuid":"bbb0b718-7ea9-46f5-9740-7c1c6722381d","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.","is_error":true,"tool_use_id":"toolu_01XqVdyDYPWtWbH29c9Pmtcz"}]},"uuid":"93dc9304-3433-4bb3-9677-0cacff2838d8","timestamp":"2026-03-11T13:48:40.019Z","toolUseResult":"User rejected tool use","sourceToolAssistantUUID":"bbb0b718-7ea9-46f5-9740-7c1c6722381d"}
{"parentUuid":"93dc9304-3433-4bb3-9677-0cacff2838d8","isSidechain":true,"userType":"external","cwd":"/workspace","sessionId":"33cd9dcf-39f6-440b-a49f-a568379ee676","version":"2.1.71","gitBranch":"HEAD","agentId":"a5343c3e31ff952a3","slug":"rippling-frolicking-kettle","type":"user","message":{"role":"user","content":[{"type":"text","text":"[Request interrupted by user for tool use]"}]},"uuid":"9a098ad5-598c-4309-839f-6edd999922b8","timestamp":"2026-03-11T13:48:40.020Z"}

View File

@@ -0,0 +1,74 @@
# Snapshot file
# Unset all aliases to avoid conflicts with functions
unalias -a 2>/dev/null || true
# Functions
# Shell Options
shopt -u autocd
shopt -u assoc_expand_once
shopt -u cdable_vars
shopt -u cdspell
shopt -u checkhash
shopt -u checkjobs
shopt -s checkwinsize
shopt -s cmdhist
shopt -u compat31
shopt -u compat32
shopt -u compat40
shopt -u compat41
shopt -u compat42
shopt -u compat43
shopt -u compat44
shopt -s complete_fullquote
shopt -u direxpand
shopt -u dirspell
shopt -u dotglob
shopt -u execfail
shopt -u expand_aliases
shopt -u extdebug
shopt -u extglob
shopt -s extquote
shopt -u failglob
shopt -s force_fignore
shopt -s globasciiranges
shopt -s globskipdots
shopt -u globstar
shopt -u gnu_errfmt
shopt -u histappend
shopt -u histreedit
shopt -u histverify
shopt -s hostcomplete
shopt -u huponexit
shopt -u inherit_errexit
shopt -s interactive_comments
shopt -u lastpipe
shopt -u lithist
shopt -u localvar_inherit
shopt -u localvar_unset
shopt -s login_shell
shopt -u mailwarn
shopt -u no_empty_cmd_completion
shopt -u nocaseglob
shopt -u nocasematch
shopt -u noexpand_translation
shopt -u nullglob
shopt -s patsub_replacement
shopt -s progcomp
shopt -u progcomp_alias
shopt -s promptvars
shopt -u restricted_shell
shopt -u shift_verbose
shopt -s sourcepath
shopt -u varredir_close
shopt -u xpg_echo
set -o braceexpand
set -o hashall
set -o interactive-comments
set -o monitor
set -o onecmd
shopt -s expand_aliases
# Aliases
# Check for rg availability
if ! (unalias rg 2>/dev/null; command -v rg) >/dev/null 2>&1; then
alias rg='/home/devuser/.local/share/zed/external_agents/claude-agent-acp/0.21.0/node_modules/\@anthropic-ai/claude-agent-sdk/vendor/ripgrep/arm64-linux/rg'
fi
export PATH=/usr/local/cargo/bin\:/usr/local/go/bin\:/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

View File

@@ -0,0 +1,9 @@
# Copier ce fichier en .env et renseigner les valeurs
# Le fichier .env est ignoré par git
# GitHub
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
# Gitea
GITEA_URL=https://gitea.example.com
GITEA_TOKEN=xxxxxxxxxxxxxxxxxxxx

View File

@@ -0,0 +1,14 @@
{
"name": "PMOCrazyCoder",
"dockerComposeFile": "docker-compose.yml",
"service": "pmocrazycoder",
"workspaceFolder": "/workspace",
"remoteUser": "devuser",
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
},
"features": {},
"postCreateCommand": "npm install -g @anthropic-ai/claude-code",
"postStartCommand": "/usr/local/bin/crazycoder-setup.sh",
"shutdownAction": "stopCompose",
}

View File

@@ -0,0 +1,28 @@
services:
pmocrazycoder:
image: niepce.petite-maison-orange.fr/public/pmocrazycoder:latest
volumes:
- ..:/workspace
- ./config/claude:/home/devuser/.claude
- ./config/opencode:/home/devuser/.config/opencode
- ./config/aichat:/home/devuser/.config/aichat
- ./config/ai_index:/workspace/.ai_index
- ./config/cargo-registry:/home/devuser/.cargo/registry
- ./config/cargo-git:/home/devuser/.cargo/git
- ./config/cargo-bin:/home/devuser/.cargo/bin
- ./config/go-cache:/home/devuser/.cache/go
- ./config/go-bin:/home/devuser/go/bin
- ./config/python-local:/home/devuser/.local
- ./config/zed:/home/devuser/.local/share/zed
- ./config/npm-cache:/home/devuser/.npm
- ./config/r-libs:/home/devuser/R
working_dir: /workspace
environment:
- LOCAL_LLM_API=http://host.docker.internal:1248/v1
- CLAUDE_CONFIG_DIR=/home/devuser/.claude
- GITHUB_TOKEN=${GITHUB_TOKEN:-}
- GITEA_URL=${GITEA_URL:-}
- GITEA_TOKEN=${GITEA_TOKEN:-}
user: devuser
entrypoint: ["/usr/local/bin/entrypoint.sh"]
command: ["bash", "-c", "tail -f /dev/null"]

5
.gitignore vendored
View File

@@ -1,3 +1,5 @@
old
reseau reseau
context context
bac_a_sable bac_a_sable
@@ -206,3 +208,6 @@ cython_debug/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc
.devcontainer/config/
pmocrazycoder-latest.oci.tar

8
.opencode/config.json Normal file
View File

@@ -0,0 +1,8 @@
{
"llm": {
"provider": "openai",
"model": "default",
"apiKey": "not-needed",
"baseURL": "${LOCAL_LLM_API:-http://host.docker.internal:1248/v1}"
}
}

28
.opencode/instructions.md Normal file
View File

@@ -0,0 +1,28 @@
# OpenCode Global Instructions Optimized for Minimal Scope
## Path handling
- Always use relative paths. Never generate absolute paths (e.g., /Users/..., /home/..., C:\...).
- All paths must be relative to the project root unless explicitly specified.
- Do not guess the current working directory. Ask for clarification if ambiguous.
- Only operate on files explicitly mentioned in the task. Never traverse directories not specified.
## Workspace assumptions
- Assume the workspace root is the repository root.
- Do not infer environment-specific values (machine, OS, home directory).
- Only read or reference files explicitly provided or mentioned. Ignore all others.
## File generation
- Generate only the requested file. Do not modify unrelated files.
- Do not add extra files or configurations unless explicitly requested.
- Output valid JSON without comments when JSON is requested.
- When accessing a file, assume no knowledge of other files unless explicitly provided.
## Determinism
- Avoid creative alternatives. Produce a single, minimal, correct implementation.
- Do not propose multiple solutions unless explicitly asked.
- Do not speculate about missing files, directories, or dependencies.
## Explicit scope limitation
- Operate strictly on files and paths explicitly mentioned in the prompt.
- Do not recursively explore the directory tree.
- Ask for clarification if necessary information is missing instead of assuming.

View File

@@ -4,9 +4,8 @@
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files // see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{ {
"inlay_hints": { "inlay_hints": {
"enabled": true "enabled": true,
}, },
"colorize_brackets": true, "colorize_brackets": true,
"project_name": "PMO Crazy Coder", "project_name": "PMO Crazy Coder",
"autosave": "on_window_change",
} }

View File

@@ -1,83 +1,104 @@
# Dockerfile FROM debian:bookworm-slim
FROM python:3.11-slim
# Ajouter le dépôt Debian Testing pour les paquets plus récents # Base system packages — no golang-go/rustc/cargo from apt (replaced by rustup + install_latest_go.sh)
RUN echo "deb http://deb.debian.org/debian testing main" \ RUN apt-get update && apt-get install -y --no-install-recommends \
> /etc/apt/sources.list.d/testing.list clang clangd pkg-config build-essential git make curl \
nodejs npm \
python3 python3-pip pylint black flake8 mypy isort \
logrotate sudo cmake zlib1g-dev \
libcurl4-openssl-dev libssl-dev \
ruby-dev libicu-dev \
jq \
r-base \
sqlite3 wget bash zsh \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Création d'un utilisateur programmeur avec UID 1000 et home dans /home/programmer # Go — latest via script only (no apt golang-go)
RUN useradd -u 1000 -m -d /home/programmer -s /bin/bash programmer COPY tools/install_latest_go.sh /tmp/install_latest_go.sh
RUN bash /tmp/install_latest_go.sh && rm /tmp/install_latest_go.sh
WORKDIR /app
# Installer les outils de développement Go et Rust
RUN apt-get update && \
apt-get install -y \
clang \
llvm-dev \
libclang-dev \
pkg-config \
build-essential \
git \
make \
curl \
nodejs \
npm \
pylint \
black \
flake8 \
mypy \
isort \
logrotate
COPY tools tools
# Installer la dernière version de Go
RUN bash tools/install_latest_go.sh
ENV PATH=/usr/local/go/bin:$PATH ENV PATH=/usr/local/go/bin:$PATH
# Installer la dernière version de Rust # Rust — rustup only (no apt rustc/cargo), stable only, no rust-docs
ENV RUSTUP_HOME=/usr/local/rustup ENV RUSTUP_HOME=/usr/local/rustup
ENV CARGO_HOME=/usr/local/cargo ENV CARGO_HOME=/usr/local/cargo
ENV PATH=/usr/local/cargo/bin:$PATH ENV PATH=/usr/local/cargo/bin:$PATH
RUN curl https://sh.rustup.rs -sSf \ RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable --no-modify-path \
| sh -s -- -y --default-toolchain stable \ && rustup component add rustfmt clippy rust-src rust-analyzer \
&& rustup install nightly \ && rustup install nightly --no-self-update \
&& rustup component add rustfmt clippy rust-src rust-docs \ && rustup component add rust-analyzer --toolchain nightly \
&& cargo install cargo-edit cargo-watch cargo-outdated cargo-make cargo-binstall # Install cargo tools via binstall (prebuilt binaries, much faster & smaller)
&& cargo install cargo-binstall \
&& cargo binstall --no-confirm \
cargo-watch cargo-outdated cargo-make \
jj-cli aichat \
&& cargo install cargo-edit \
&& cargo install --git https://github.com/nilskch/jj-lsp.git \
# Clean build cache (keep installed binaries, drop registry & build artifacts)
&& rm -rf $CARGO_HOME/registry $CARGO_HOME/git /tmp/cargo-*
# Installer la dernière version de typscript # Node tools
RUN npm install -g typescript tsc @types/node eslint prettier RUN npm install -g typescript @types/node eslint prettier pyright typescript-language-server \
mcp-server-commands \
&& npm cache clean --force
RUN cargo binstall --strategies crate-meta-data jj-cli # Ruby tools
RUN gem install github-linguist \
&& gem cleanup \
&& rm -rf /var/cache/gem /root/.gem/specs
# Installer le SDK MCP Python # Python tools
RUN pip install --no-cache-dir ruff fastmcp ipython RUN pip install --no-cache-dir --break-system-packages ruff fastmcp ipython tasklin
RUN curl -fsSL https://opencode.ai/install | bash \ # Go tools
&& mv /root/.opencode/bin/opencode /usr/local/bin/ ENV GOPATH=/usr/local/go
ENV GOBIN=/usr/local/go/bin
RUN go install golang.org/x/tools/gopls@latest \
&& go install github.com/isaacphi/mcp-language-server@latest \
&& go install github.com/github/github-mcp-server/cmd/github-mcp-server@latest \
&& go install gitea.com/gitea/gitea-mcp@latest \
&& go clean -cache -modcache
# Copier le serveur # Shell completions
COPY src . RUN mkdir -p /etc/bash_completion.d \
&& curl -fsSL https://raw.githubusercontent.com/sigoden/aichat/main/scripts/completions/aichat.bash \
> /etc/bash_completion.d/aichat \
&& jj util completion bash > /etc/bash_completion.d/jj
# Copier le script de démarrage # Non-root user
COPY docker_startup.sh . ARG USERNAME=devuser
COPY opencode.json ~/.config/opencode ARG USER_UID=1000
ARG USER_GID=1000
RUN groupadd --gid $USER_GID $USERNAME && \
useradd --uid $USER_UID --gid $USER_GID -m $USERNAME --shell /bin/bash && \
echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME && \
chmod 440 /etc/sudoers.d/$USERNAME
# Exposer le port COPY docker-entrypoint.sh /usr/local/bin/entrypoint.sh
EXPOSE 7860 RUN chmod +x /usr/local/bin/entrypoint.sh
ENV OLLAMA_ADDR="http://host.docker.internal:11434"
# Rendre le script exécutable et lancer le serveur COPY bin/* /usr/local/bin/
RUN chmod +x docker_startup.sh RUN chmod +x /usr/local/bin/*
RUN mkdir /sandbox /log \ RUN mkdir -p /home/${USERNAME}/.config \
&& chown programmer:programmer /sandbox /log && mkdir -p /home/${USERNAME}/.cache \
&& chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.config \
&& chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.cache
COPY --chown=${USERNAME}:${USERNAME} opencode /home/${USERNAME}/.config/opencode
# Création de la configuration logrotate USER $USERNAME
COPY logrotate-config /etc/logrotate.d/mcp-logs WORKDIR /workspace
# Changer l'utilisateur pour programmeur ARG OCI_VERSION=latest
USER programmer ARG OCI_CREATED=unknown
RUN mkdir -p /home/programmer/.config/opencode ARG OCI_REVISION=unknown
COPY opencode.json /home/programmer/.config/opencode
CMD ["./docker_startup.sh"] LABEL org.opencontainers.image.title="PMOCrazyCoder" \
org.opencontainers.image.description="AI-powered dev container with Rust, Go, Python, Node, Jujutsu, Claude Code and opencode" \
org.opencontainers.image.url="https://niepce.petite-maison-orange.fr/public/pmocrazycoder" \
org.opencontainers.image.source="https://niepce.petite-maison-orange.fr/public/pmocrazycoder" \
org.opencontainers.image.version="${OCI_VERSION}" \
org.opencontainers.image.created="${OCI_CREATED}" \
org.opencontainers.image.revision="${OCI_REVISION}" \
org.opencontainers.image.licenses="CECILL-2.1"
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["/bin/bash"]

190
Makefile
View File

@@ -1,36 +1,168 @@
.PHONY: build run all debug # =============================================================================
# Image identity
# =============================================================================
# Variables IMAGE_NAME := pmocrazycoder
IMAGE_NAME = pmo_crazy_coder IMAGE_TAG := latest
TAG = latest
PORT = 7860
SANDBOX_VOLUME = "./bac_a_sable"
CONTAINER_NAME = $(IMAGE_NAME)
DOCKER_RUN_COMMON = --rm -p $(PORT):$(PORT) -v $(SANDBOX_VOLUME):/sandbox --name $(CONTAINER_NAME)
FULL_IMAGE_NAME = $(IMAGE_NAME):$(TAG)
all: run # =============================================================================
# Registry
# =============================================================================
$(SANDBOX_VOLUME): REGISTRY := niepce.petite-maison-orange.fr/public
mkdir -p $(SANDBOX_VOLUME)
stampdir: # =============================================================================
mkdir -p stamp # Build platforms
# =============================================================================
stamp/build_PMOCrazyCoder: stampdir Dockerfile PLATFORMS := linux/amd64,linux/arm64
@echo building the PMOCrazyCoder server
@docker build -t $(FULL_IMAGE_NAME) .
@touch $@
build: stamp/build_PMOCrazyCoder # =============================================================================
run: build $(SANDBOX_VOLUME) # Derived image identifiers
@echo Launching the PMOCrazyCoder MCP server # =============================================================================
@docker run $(DOCKER_RUN_COMMON) \
-it \
$(FULL_IMAGE_NAME)
debug: build $(SANDBOX_VOLUME) FULL_IMAGE := $(REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG)
@echo Launching the PMOCrazyCoder MCP server in debug mode OCI_ARCHIVE := $(IMAGE_NAME)-$(IMAGE_TAG).oci.tar
@docker run $(DOCKER_RUN_COMMON) \
-it \ # =============================================================================
$(FULL_IMAGE_NAME) /bin/bash # Docker buildx builder
# =============================================================================
BUILDER := $(IMAGE_NAME)-builder
# =============================================================================
# OCI image annotations (injected via --build-arg)
# =============================================================================
OCI_VERSION := $(IMAGE_TAG)
OCI_CREATED := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
OCI_REVISION := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
# =============================================================================
# Build context
# =============================================================================
PROJECT_DIR := $(shell pwd)
BUILD_CONTEXT := .
DOCKERFILE := Dockerfile
BUILD_ARGS := \
--build-arg OCI_VERSION=$(OCI_VERSION) \
--build-arg OCI_CREATED=$(OCI_CREATED) \
--build-arg OCI_REVISION=$(OCI_REVISION)
# =============================================================================
# Dev container
# =============================================================================
WORKSPACE := /workspace
CONTAINER_NAME := ai-dev-container
MCP_LOG := $(WORKSPACE)/.ai_index/mcp.log
# =============================================================================
# Targets
# =============================================================================
.PHONY: all build build-multiplatform push run run-mcp \
init-index mcp-list embeddings update-graph \
restart-mcp bootstrap clean clean-builder help
## all : Build multi-platform image, push to registry
all: build-multiplatform push
## build : Build for the native platform and load into the local Docker daemon
build:
docker build $(BUILD_ARGS) \
--tag $(FULL_IMAGE) \
--file $(DOCKERFILE) \
$(BUILD_CONTEXT)
## build-multiplatform : Build for $(PLATFORMS) and export as a local OCI archive
build-multiplatform: .builder-ready
docker buildx build $(BUILD_ARGS) \
--builder $(BUILDER) \
--platform $(PLATFORMS) \
--tag $(FULL_IMAGE) \
--file $(DOCKERFILE) \
--provenance=false \
--output type=oci,dest=$(OCI_ARCHIVE) \
$(BUILD_CONTEXT)
# Create the buildx builder with docker-container driver (once)
.builder-ready:
docker buildx inspect $(BUILDER) > /dev/null 2>&1 \
|| docker buildx create --name $(BUILDER) --driver docker-container --bootstrap
touch .builder-ready
## push : Push the OCI archive to the registry using skopeo, then pull locally
push: $(OCI_ARCHIVE)
skopeo copy \
--dest-tls-verify=true \
--multi-arch all \
oci-archive:$(OCI_ARCHIVE) \
docker://$(FULL_IMAGE)
docker pull $(FULL_IMAGE)
# Ensure the OCI archive exists before pushing
$(OCI_ARCHIVE):
$(MAKE) build-multiplatform
## run : Run interactive container with mounted project
run:
docker run -v $(PROJECT_DIR):$(WORKSPACE) -it --rm $(FULL_IMAGE)
## run-mcp : Run container with MCP server in background
run-mcp:
docker run -v $(PROJECT_DIR):$(WORKSPACE) -it --rm $(FULL_IMAGE) bash
## init-index : Initialize .ai_index directory structure
init-index:
@echo "[MAKE] Initializing .ai_index..."
mkdir -p $(PROJECT_DIR)/.ai_index/ast
mkdir -p $(PROJECT_DIR)/.ai_index/embeddings
@if [ ! -f $(PROJECT_DIR)/.ai_index/graph.db ]; then \
sqlite3 $(PROJECT_DIR)/.ai_index/graph.db "VACUUM;"; \
fi
## mcp-list : Run MCP list_imports tool
mcp-list:
@echo "[MAKE] Running MCP list_imports.py..."
python3 $(PROJECT_DIR)/opt/dev-tools/list_imports.py --project $(WORKSPACE)
## embeddings : Generate embeddings
embeddings:
@echo "[MAKE] Generating embeddings..."
python3 $(PROJECT_DIR)/opt/dev-tools/generate_embeddings.py --project $(WORKSPACE)
## update-graph : Update dependency graph
update-graph:
@echo "[MAKE] Updating dependency graph..."
python3 $(PROJECT_DIR)/opt/dev-tools/update_graph.py --project $(WORKSPACE)
## restart-mcp : Restart MCP server inside running container
restart-mcp:
docker exec -it $(CONTAINER_NAME) bash -c "\
pkill -f mcp-server; \
/opt/bin/mcp-server --project $(WORKSPACE) > $(MCP_LOG) 2>&1 & \
"
## bootstrap : Build container and init index
bootstrap: build init-index
@echo "[MAKE] Bootstrap complete. Run 'make run' to start the container."
## clean : Remove OCI archive, local image and generated files
clean: clean-builder
docker rmi --force $(FULL_IMAGE) 2>/dev/null || true
rm -f $(OCI_ARCHIVE)
rm -f $(PROJECT_DIR)/.ai_index/*.log
rm -rf $(PROJECT_DIR)/.ai_index/ast/*
rm -rf $(PROJECT_DIR)/.ai_index/embeddings/*
## clean-builder : Remove the buildx builder
clean-builder:
docker buildx rm $(BUILDER) 2>/dev/null || true
rm -f .builder-ready
## help : Show this help
help:
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## //'

View File

@@ -1,55 +0,0 @@
```bash
jj log | orla agent -m ollama:qwen3:0.6b \
"
Transform the following `jj log` output into JSON according to the given schema.
**Return only valid JSON, without any Markdown formatting :**
- no backticks,
- no code blocks,
- no extra text
Schema:
{
\"type\": \"array\",
\"items\": {
\"type\": \"object\",
\"properties\": {
\"hash\": {\"type\": \"string\"},
\"author\": {\"type\": \"string\"},
\"date\": {\"type\": \"string\"},
\"message\": {\"type\": \"string\"}
},
\"required\": [\"hash\", \"author\", \"date\", \"message\"]
}
}
" | grep -v '```' | jq .
```
```json
[
{
"hash": "252b5de3",
"author": "nrrwwpms",
"date": "2026-01-25 17:36:18",
"message": "(no description set)"
},
{
"hash": "9f9d9bc3",
"author": "stxkzlul",
"date": "2026-01-25 12:08:12",
"message": "Ajout de Dockerfile et configuration MCP"
},
{
"hash": "7412b51a",
"author": "xnrxukn",
"date": "2026-01-25 07:53:42",
"message": "Initial commit"
}
]
```
Ok, on va essayer de développer un outil un peu plus avancé.
Dans le fichier @src/sandbox/__init__.py Nous allons créer une fonction jj_log.
Cette fonction prendra en paramètres:
- un projet

197
README.md
View File

@@ -1,3 +1,196 @@
# MonMCP # PMOCrazyCoder
Comprendre quelque chose au serveur MCP. Environnement de développement conteneurisé avec assistance IA intégrée — Go, Rust, Python, TypeScript, R, Ruby — et outillage IA local (LM Studio) ou cloud (Claude Pro).
## Vision
> Un container de développement complet, multi-langages, avec LSP, outils IA et MCP intégrés,
> utilisable depuis Zed, VS Code ou Cursor, avec persistance par projet.
## Ce que contient l'image
### Langages et compilateurs
- **Go** (dernière version stable via script)
- **Rust** (stable + nightly, avec rustfmt, clippy, rust-analyzer, rust-src)
- **Python 3** (avec ruff, fastmcp, ipython, tasklin)
- **Node.js / npm** (avec TypeScript, ESLint, Prettier, Pyright, typescript-language-server)
- **Ruby** (avec github-linguist)
- **R**
- **Clang / LLVM** (clangd inclus)
### Outils de développement
- `jj` (Jujutsu VCS) + `jj-lsp`
- `gopls`, `mcp-language-server`
- `cargo-edit`, `cargo-watch`, `cargo-outdated`, `cargo-make`, `cargo-binstall`
- `sqlite3`, `cmake`, `make`, `git`
- `aichat` (client LLM CLI)
### MCP Servers installés
Le conteneur inclut plusieurs MCP servers pour l'intégration avec les outils de développement :
| MCP Server | Utilisation |
|---|---|
| `mcp-language-server` | LSP pour Go (gopls), Python (pyright-langserver), Rust (rust-analyzer), TypeScript |
| `github-mcp-server` | Intégration GitHub (repos, issues, PR) via `GITHUB_TOKEN` |
| `gitea-mcp` | Intégration Gitea/Forgejo via `GITEA_URL` et `GITEA_TOKEN` |
| `mcp-server-commands` | Exécution de commandes shell depuis l'assistant IA |
### Assistants IA
- **Claude Code** (installé au `postCreateCommand`)
- **opencode** (configuré via l'entrypoint)
- **aichat** (configuré via l'entrypoint, connecté au LLM local)
### Configuration MCP dans les assistants
Les MCP servers sont automatiquement configurés dans opencode et Claude Code via `crazycoder-setup.sh` (exécuté en `postStartCommand`) :
**opencode** (`~/.config/opencode/opencode.json`) :
- LSP pour Go, Python, Rust, TypeScript via `mcp-language-server`
- Commandes shell via `mcp-server-commands`
- GitHub MCP (si `GITHUB_TOKEN` défini)
- Gitea MCP (si `GITEA_URL` et `GITEA_TOKEN` définis)
**Claude Code** (`~/.claude/.claude.json`) :
- Même configuration MCP qu'opencode
Les configurations sont générées dynamiquement au démarrage du conteneur avec les variables d'environnement disponibles.
## Variables d'environnement (.env)
Le fichier `.env` (non versionné, ignoré par git) contient les credentials pour les services externes :
| Variable | Description | Exemple |
|---|---|---|
| `GITHUB_TOKEN` | Personal Access Token GitHub pour l'accès API | `ghp_xxxxxxxxxxxxxxxxxxxx` |
| `GITEA_URL` | URL de votre serveur Gitea/Forgejo | `https://gitea.example.com` |
| `GITEA_TOKEN` | Token d'accès API Gitea | `xxxxxxxxxxxxxxxxxxxx` |
Copiez `.devcontainer/.env.example` vers `.env` et renseignez vos tokens :
```bash
cp .devcontainer/.env.example .env
# Éditer .env avec vos credentials
```
Ces variables sont utilisées par :
- Le MCP Server GitHub (pour les opérations repo/issues/PR)
- Le MCP Server Gitea (intégration avec votre instance)
## Utilisation rapide
### Ajouter l'environnement à un projet existant
```bash
curl -fsSL https://gargoton.petite-maison-orange.fr/eric/PMOCrazycoder/raw/branch/main/setup-devcontainer.sh | bash
```
Ou avec un endpoint LLM personnalisé :
```bash
LOCAL_LLM_API=http://monserveur:1248/v1 bash setup-devcontainer.sh
```
Le script télécharge les fichiers `devcontainer.json` et `docker-compose.yml` directement depuis le dépôt Gitea, crée les répertoires de persistance dans `.devcontainer/config/` et met à jour `.gitignore`.
### Ouvrir dans le container
Ouvrir le projet dans VS Code, Zed ou Cursor et choisir "Reopen in Container". L'image est tirée automatiquement depuis le registry.
### Authentifier Claude Code
Dans le panneau Claude Agent de Zed, lancer `/login` et choisir "Log in with Claude.ai" (abonnement Pro/Max).
## Persistance par projet
Toutes les données et caches sont persistés dans `.devcontainer/config/` (ignoré par git) :
| Répertoire | Contenu |
|---|---|
| `config/claude/` | Sessions et config Claude Code |
| `config/opencode/` | Config opencode |
| `config/aichat/` | Config aichat |
| `config/ai_index/` | Index de code du projet |
| `config/cargo-registry/` | Cache crates.io |
| `config/cargo-git/` | Cache dépendances git Cargo |
| `config/cargo-bin/` | Binaires Rust installés pour le projet |
| `config/go-cache/` | Cache modules Go |
| `config/go-bin/` | Binaires Go installés pour le projet |
| `config/python-local/` | Paquets pip user + gems Ruby |
| `config/npm-cache/` | Cache npm |
| `config/r-libs/` | Paquets R installés |
### Installer des binaires persistants
```bash
# Rust — persiste dans .devcontainer/config/cargo-bin/
cargo install cargo-nextest
# Go — persiste dans .devcontainer/config/go-bin/
GOBIN=~/go/bin go install github.com/some/tool@latest
# Python — persiste dans .devcontainer/config/python-local/
pip install --user monpaquet
# R — persiste dans .devcontainer/config/r-libs/
R -e 'install.packages("tidyverse")'
```
## Configuration MCP détaillée
### opencode
Le fichier `~/.config/opencode/opencode.json` est généré à chaque démarrage avec :
- **LSP servers** : gopls, pyright-langserver, rust-analyzer, typescript-language-server
- **Shell commands** : `mcp-server-commands`
- **GitHub MCP** : activé si `GITHUB_TOKEN` est défini
- **Gitea MCP** : activé si `GITEA_URL` et `GITEA_TOKEN` sont définis
- **Provider LM Studio** : endpoint local configurable
### Claude Code
Le fichier `~/.claude/.claude.json` est mis à jour avec la section `mcpServers` contenant les mêmes serveurs que opencode.
## LLM local
L'entrypoint configure automatiquement opencode et aichat pour pointer sur `LOCAL_LLM_API`
(défaut : `http://host.docker.internal:1248/v1`).
Variables d'environnement disponibles dans le container :
| Variable | Défaut | Description |
|---|---|---|
| `LOCAL_LLM_API` | `http://host.docker.internal:1248/v1` | Endpoint OpenAI-compatible |
| `LLM_MODEL` | `ollama:qwen/qwen3-coder-next@4bit` | Modèle à utiliser |
| `TEMPERATURE` | `0.5` | Température |
| `NUM_CTX` | `32768` | Taille de contexte |
| `MAXTOKEN` | `4096` | Tokens max en sortie |
## Construire et publier l'image
```bash
# Build natif (local)
make build
# Build multi-arch (amd64 + arm64) + push vers le registry
make all
# Push uniquement (si l'archive OCI existe déjà)
make push
# Lister toutes les targets
make help
```
L'image est publiée sur `niepce.petite-maison-orange.fr/public/pmocrazycoder:latest`.
Le push utilise `skopeo` — assurez-vous d'être authentifié sur le registry.
## Sources
- Dépôt git : `https://gargoton.petite-maison-orange.fr/eric/PMOCrazycoder`
- Registry OCI : `niepce.petite-maison-orange.fr/public/pmocrazycoder:latest`
## Licence
CECILL-2.1

191
bin/crazycoder-setup.sh Normal file
View File

@@ -0,0 +1,191 @@
#!/bin/bash
# crazycoder-setup.sh
# Runs via postStartCommand — configures aichat, opencode and Claude Code MCP.
# Executed by Zed/VS Code/Cursor after the container starts, as remoteUser (devuser).
export USERNAME=${USERNAME:-devuser}
export WORKSPACE=${WORKSPACE:-/workspace}
export LOCAL_LLM_API=${LOCAL_LLM_API:-http://host.docker.internal:1248/v1}
export LLM_MODEL=${LLM_MODEL:-'ollama:qwen/qwen3-coder-next@4bit'}
export AICHAT_API_BASE=$(sed -E 's@(https?://[^/]+).*$@\1@' <<< $LOCAL_LLM_API)
export MAXTOKEN=${MAXTOKEN:-4096}
export NUM_CTX=${NUM_CTX:-32768}
export REPEAT_PENALTY=${REPEAT_PENALTY:-1.3}
export TEMPERATURE=${TEMPERATURE:-0.5}
export TOP_P=${TOP_P:-0.95}
export TOP_K=${TOP_K:-40}
export STOP=${STOP:-"[\"</s>\", \"\\n\\n\", \"def \", \"class \"]"}
# --- Clear Zed external_agents so Zed can re-download without "Directory not empty" ---
rm -rf "$HOME/.local/share/zed/external_agents"
# --- Initialise .ai_index si nécessaire ---
if [ ! -d "$WORKSPACE/.ai_index" ]; then
mkdir -p "$WORKSPACE/.ai_index/ast"
mkdir -p "$WORKSPACE/.ai_index/embeddings"
touch "$WORKSPACE/.ai_index/graph.db"
fi
# --- Configure PATH dans .bashrc (une seule fois) ---
if ! grep -q "LOCAL_LLM_API" "$HOME/.bashrc"; then
echo "export LOCAL_LLM_API=$LOCAL_LLM_API" >> "$HOME/.bashrc"
echo "export AICHAT_API_BASE=$AICHAT_API_BASE" >> "$HOME/.bashrc"
echo "export PATH=\$HOME/.cargo/bin:\$HOME/go/bin:\$HOME/.local/bin:\$PATH" >> "$HOME/.bashrc"
fi
# --- Configure aichat ---
mkdir -p "$HOME/.config/aichat"
cat > "$HOME/.config/aichat/config.yaml" <<EOF
model: lmstudio:${LLM_MODEL#*:}
clients:
- type: openai-compatible
name: lmstudio
api_base: ${LOCAL_LLM_API}
api_key: placeholder
models:
- name: ${LLM_MODEL#*:}
max_input_tokens: ${NUM_CTX}
temperature: ${TEMPERATURE}
top_p: ${TOP_P}
EOF
# --- Configure opencode ---
RUST_ANALYZER=$(rustup which --toolchain nightly rust-analyzer 2>/dev/null || true)
mkdir -p "$HOME/.config/opencode"
jq -n \
--arg workspace "$WORKSPACE" \
--arg gopath "$GOPATH" \
--arg rust_analyzer "$RUST_ANALYZER" \
--arg llm_api "$LOCAL_LLM_API" \
--arg llm_model "${LLM_MODEL#*:}" \
--arg full_model "$LLM_MODEL" \
--arg github_token "${GITHUB_TOKEN:-}" \
--arg gitea_url "${GITEA_URL:-}" \
--arg gitea_token "${GITEA_TOKEN:-}" \
--argjson temperature "$TEMPERATURE" \
--argjson top_p "$TOP_P" \
--argjson top_k "$TOP_K" \
--argjson max_tokens "$MAXTOKEN" \
--argjson num_ctx "$NUM_CTX" \
--argjson repeat_penalty "$REPEAT_PENALTY" \
--argjson stop "$STOP" \
'{
"$schema": "https://opencode.ai/config.json",
"mcp": ({
"go-lsp-tools": {
"type": "local",
"command": ["mcp-language-server", "--workspace", $workspace, "--lsp", "gopls"],
"enabled": true,
"environment": { "GOPATH": $gopath }
},
"python-lsp-tools": {
"type": "local",
"command": ["mcp-language-server", "--workspace", $workspace, "--lsp", "pyright-langserver", "--", "--stdio"],
"enabled": true
},
"rust-lsp-tools": {
"type": "local",
"command": ["mcp-language-server", "--workspace", $workspace, "--lsp", $rust_analyzer],
"enabled": true
},
"typescript-lsp-tools": {
"type": "local",
"command": ["mcp-language-server", "--workspace", $workspace, "--lsp", "typescript-language-server", "--", "--stdio"],
"enabled": true
},
"shell": {
"type": "local",
"command": ["mcp-server-commands"],
"enabled": true
},
"github": (if $github_token != "" then {
"type": "local",
"command": ["github-mcp-server", "stdio"],
"enabled": true,
"environment": { "GITHUB_PERSONAL_ACCESS_TOKEN": $github_token }
} else null end),
"gitea": (if ($gitea_token != "" and $gitea_url != "") then {
"type": "local",
"command": ["gitea-mcp", "-t", "stdio", "--host", $gitea_url, "--token", $gitea_token],
"enabled": true
} else null end)
} | with_entries(select(.value != null))),
"provider": {
"lmstudio": {
"npm": "@ai-sdk/openai-compatible",
"name": "LM Studio (local)",
"options": { "baseURL": $llm_api, "apiKey": "" },
"models": {
"qwen/qwen3-coder-next@4bit": {
"name": "Qwen 3 Coder Next 80B — (LLMStudio ; 4-bit)",
"limit": { "context": 200000, "output": 65536 },
"settings": { "temperature": $temperature, "top_p": $top_p, "top_k": $top_k, "max_tokens": $max_tokens, "num_ctx": $num_ctx, "repeat_penalty": $repeat_penalty, "stop": $stop }
},
"qwen/qwen3-coder-next@6bit": {
"name": "Qwen 3 Coder Next 80B — (LLMStudio ; 6-bit)",
"limit": { "context": 200000, "output": 65536 },
"settings": { "temperature": $temperature, "top_p": $top_p, "top_k": $top_k, "max_tokens": $max_tokens, "num_ctx": $num_ctx, "repeat_penalty": $repeat_penalty, "stop": $stop }
}
}
}
},
"model": $full_model
}' > "$HOME/.config/opencode/opencode.json"
# --- Configure MCP pour Claude Code ---
MCP_SERVERS=$(jq -n \
--arg workspace "$WORKSPACE" \
--arg gopath "$GOPATH" \
--arg rust_analyzer "$RUST_ANALYZER" \
--arg github_token "${GITHUB_TOKEN:-}" \
--arg gitea_url "${GITEA_URL:-}" \
--arg gitea_token "${GITEA_TOKEN:-}" \
'{
"go-lsp-tools": {
"type": "stdio",
"command": "mcp-language-server",
"args": ["--workspace", $workspace, "--lsp", "gopls"],
"env": { "GOPATH": $gopath }
},
"python-lsp-tools": {
"type": "stdio",
"command": "mcp-language-server",
"args": ["--workspace", $workspace, "--lsp", "pyright-langserver", "--", "--stdio"]
},
"rust-lsp-tools": {
"type": "stdio",
"command": "mcp-language-server",
"args": ["--workspace", $workspace, "--lsp", $rust_analyzer]
},
"typescript-lsp-tools": {
"type": "stdio",
"command": "mcp-language-server",
"args": ["--workspace", $workspace, "--lsp", "typescript-language-server", "--", "--stdio"]
},
"github": (if $github_token != "" then {
"type": "stdio",
"command": "github-mcp-server",
"args": ["stdio"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": $github_token }
} else null end),
"shell": {
"type": "stdio",
"command": "mcp-server-commands"
},
"gitea": (if ($gitea_token != "" and $gitea_url != "") then {
"type": "stdio",
"command": "gitea-mcp",
"args": ["-t", "stdio", "--host", $gitea_url, "--token", $gitea_token]
} else null end)
} | with_entries(select(.value != null))')
CLAUDE_JSON="$HOME/.claude/.claude.json"
mkdir -p "$HOME/.claude"
if [ -f "$CLAUDE_JSON" ]; then
jq --argjson mcp "$MCP_SERVERS" '. + {mcpServers: $mcp}' "$CLAUDE_JSON" > "${CLAUDE_JSON}.tmp" \
&& mv "${CLAUDE_JSON}.tmp" "$CLAUDE_JSON"
else
jq -n --argjson mcp "$MCP_SERVERS" '{mcpServers: $mcp}' > "$CLAUDE_JSON"
fi

48
bin/jj-ai-commit.sh Executable file
View File

@@ -0,0 +1,48 @@
#!/bin/bash
LOCAL_LLM_API=${LOCAL_LLM_API:-http://host.docker.internal:1248/v1}
MODEL="${LLM_MODEL:-qwen3-coder}"
llm_query() {
local content="$1"
jq -n --arg content "$content" --arg model "$MODEL" \
'{"model": $model, "messages": [{"role": "user", "content": $content}]}' \
| curl -s -X POST \
-H "Content-Type: application/json" \
-d @- \
"$LOCAL_LLM_API/chat/completions" \
| jq -r '.choices[0].message.content'
}
# Get list of modified files
FILES=$(jj diff --summary | awk '{print $2}')
if [[ -z "$FILES" ]]; then
echo "No changes to commit." >&2
exit 1
fi
# Pass 1: summarize each file diff individually
SUMMARIES=""
while IFS= read -r file; do
FILE_DIFF=$(jj diff "$file" 2>/dev/null)
if [[ -z "$FILE_DIFF" ]]; then
continue
fi
PROMPT="Résume en 2-3 phrases les changements apportés dans ce diff pour le fichier '$file'. Sois concis et factuel.
$FILE_DIFF"
SUMMARY=$(llm_query "$PROMPT")
SUMMARIES="${SUMMARIES}### $file
$SUMMARY
"
done <<< "$FILES"
# Pass 2: generate commit message from summaries
COMMIT_PROMPT="À partir des résumés suivants de fichiers modifiés, génère un message de commit git.
Réponds UNIQUEMENT avec un objet JSON valide contenant \"title\" (string, max 72 chars, en anglais, style imperative) et \"message\" (string, détails en français). Pas de texte avant ou après le JSON.
$SUMMARIES"
llm_query "$COMMIT_PROMPT" | jq -r '.title + "\n\n" + .message'

17
docker-entrypoint.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
# docker-entrypoint.sh — minimal, just sets up PATH and execs the command
export PATH=/usr/local/cargo/bin:/usr/local/go/bin:$HOME/.local/bin:$PATH
# Set npm prefix to user directory so global installs don't require root
npm config set prefix "$HOME/.local"
# Clear Zed external_agents on each start so Zed can re-download without
# hitting "Directory not empty" on atomic rename.
rm -rf "$HOME/.local/share/zed/external_agents"
if [ -z "$1" ]; then
exec /bin/bash
else
exec "$@"
fi

View File

@@ -1,13 +0,0 @@
#!/bin/bash
# Docker Startup Script for MCP Server and Open Code
# This script launches the MCP server in background and starts Open Code in interactive mode
echo "Starting MCP server in background..."
python3 PMOCrazyCoder.py >/log/server.log 2>&1 &
# Give the server a moment to start
sleep 3
echo "Starting Open Code in interactive mode..."
opencode

View File

@@ -1,13 +0,0 @@
/log/server.log /log/opencode.log {
daily
rotate 5
compress
delaycompress
missingok
notifempty
create 644 programmer programmer
sharedscripts
postrotate
# No restart needed for log files
endscript
}

View File

@@ -1,9 +0,0 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"PMO-Crazy-coder": {
"type": "remote",
"url": "http://localhost:7860/mcp"
}
}
}

49
opencode/AGENTs.md Normal file
View File

@@ -0,0 +1,49 @@
# AGENTS.md - Development Guidelines
## Code Style Guidelines
### General Principles
- Use relative paths only (never absolute paths)
- Operate strictly on explicitly mentioned files
- Generate minimal, single implementations
- Avoid speculation about missing files
## Error Handling
- Use structured error messages
- Avoid verbose stack traces in user-facing output
## Naming Conventions
- Variables/functions: `snake_case`
- Types/interfaces: `PascalCase`
- Constants: `UPPER_SNAKE_CASE`
## CRITICAL Security Rules
### File Operations
- **NEVER** delete files without explicit human authorization
- **NEVER** modify files outside the scope of the requested task
- Always use relative paths (never absolute paths)
### Version Control with `jj`
- Projects are managed with `jj` (Jujutsu), NOT Git
- **Before starting modifications to respond to a user prompt**, run `jj new` to create a commit
- This creates a baseline to revert all changes from that prompt if needed
- Do NOT run `jj new` for every individual file change—only once at the start of a prompt response
## File Generation Rules
- Output only requested files
- Do not modify unrelated files
- Valid JSON without comments when requested
- Assume no knowledge of other files unless provided
## MCP Tools
- Located in `/opt/dev-tools/`
- Use SQLite for graph storage (`.ai_index/graph.db`)
- Embeddings stored in `.ai_index/embeddings/`
- AST data in `.ai_index/ast/`
## LM Studio Integration
- Local LLM API: `http://host.docker.internal:1248`
- Model: `qwen/qwen3-coder-next@4bit`
- Context window: 32768 tokens
- Max output: 65536 tokens

32
pyproject.toml Normal file
View File

@@ -0,0 +1,32 @@
[project]
name = "pmocrazykoder"
version = "0.1.0"
description = "A crazy MCP server"
requires-python = ">=3.10"
dependencies = [
]
[project.optional-dependencies]
dev = [
"ruff",
"black",
"mypy",
"isort",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W"]
ignore = []
[tool.black]
target-version = ["py310"]
[tool.mypy]
python_version = "3.10"

169
setup-devcontainer.sh Executable file
View File

@@ -0,0 +1,169 @@
#!/usr/bin/env bash
# setup-devcontainer.sh
#
# Adds the PMOCrazyCoder devcontainer environment to any project.
# Usage: bash setup-devcontainer.sh [project-dir]
#
# Example (run from the project root):
# bash setup-devcontainer.sh
#
# Or download and run directly:
# curl -fsSL https://gargoton.petite-maison-orange.fr/eric/PMOCrazycoder/raw/branch/main/setup-devcontainer.sh | bash
set -euo pipefail
PROJECT_DIR="${1:-$(pwd)}"
DEVCONTAINER_DIR="$PROJECT_DIR/.devcontainer"
LOCAL_LLM_API="${LOCAL_LLM_API:-http://host.docker.internal:1248/v1}"
GIT_RAW="https://gargoton.petite-maison-orange.fr/eric/PMOCrazycoder/raw/branch/main"
# --- Colors ---
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RESET='\033[0m'
info() { echo -e "${GREEN}[setup]${RESET} $*"; }
warning() { echo -e "${YELLOW}[setup]${RESET} $*"; }
error() { echo -e "${RED}[setup]${RESET} $*" >&2; exit 1; }
fetch() {
local src="$1" dst="$2"
curl -fsSL "$GIT_RAW/$src" -o "$dst" \
|| error "Failed to fetch $src from $GIT_RAW"
}
# --- Checks ---
[[ -d "$PROJECT_DIR" ]] || error "Directory not found: $PROJECT_DIR"
command -v curl >/dev/null || error "curl is required"
# --- .devcontainer/ ---
info "Creating .devcontainer structure in $PROJECT_DIR"
mkdir -p "$DEVCONTAINER_DIR/config/claude"
mkdir -p "$DEVCONTAINER_DIR/config/opencode"
mkdir -p "$DEVCONTAINER_DIR/config/aichat"
mkdir -p "$DEVCONTAINER_DIR/config/ai_index"
mkdir -p "$DEVCONTAINER_DIR/config/cargo-registry"
mkdir -p "$DEVCONTAINER_DIR/config/cargo-git"
mkdir -p "$DEVCONTAINER_DIR/config/cargo-bin"
mkdir -p "$DEVCONTAINER_DIR/config/go-cache"
mkdir -p "$DEVCONTAINER_DIR/config/go-bin"
mkdir -p "$DEVCONTAINER_DIR/config/python-local"
mkdir -p "$DEVCONTAINER_DIR/config/zed"
mkdir -p "$DEVCONTAINER_DIR/config/npm-cache"
mkdir -p "$DEVCONTAINER_DIR/config/r-libs"
# --- devcontainer.json ---
DEVCONTAINER_JSON="$DEVCONTAINER_DIR/devcontainer.json"
if [[ -f "$DEVCONTAINER_JSON" ]]; then
warning "devcontainer.json already exists, skipping"
else
fetch ".devcontainer/devcontainer.json" "$DEVCONTAINER_JSON"
PROJECT_NAME="$(basename "$PROJECT_DIR")"
SERVICE_NAME="crazycoder-${PROJECT_NAME}"
# Patch display name and service reference
sed -i.bak \
-e "s/\"name\": \"PMOCrazyCoder\"/\"name\": \"${PROJECT_NAME}\"/" \
-e "s/\"service\": \"pmocrazycoder\"/\"service\": \"${SERVICE_NAME}\"/" \
"$DEVCONTAINER_JSON"
rm -f "$DEVCONTAINER_JSON.bak"
info "Created devcontainer.json"
fi
# --- docker-compose.yml ---
COMPOSE_FILE="$DEVCONTAINER_DIR/docker-compose.yml"
if [[ -f "$COMPOSE_FILE" ]]; then
warning "docker-compose.yml already exists, skipping"
else
fetch ".devcontainer/docker-compose.yml" "$COMPOSE_FILE"
PROJECT_NAME="$(basename "$PROJECT_DIR")"
SERVICE_NAME="crazycoder-${PROJECT_NAME}"
# Rename the service to avoid collision with project's own Docker services
sed -i.bak "s/^ pmocrazycoder:/ ${SERVICE_NAME}:/" "$COMPOSE_FILE"
# Patch LOCAL_LLM_API if overridden
if [[ "$LOCAL_LLM_API" != "http://host.docker.internal:1248/v1" ]]; then
sed -i.bak "s|http://host.docker.internal:1248/v1|${LOCAL_LLM_API}|g" "$COMPOSE_FILE"
fi
rm -f "$COMPOSE_FILE.bak"
info "Created docker-compose.yml"
fi
# --- .env ---
ENV_FILE="$DEVCONTAINER_DIR/.env"
if [[ -f "$ENV_FILE" ]]; then
warning ".env already exists, skipping"
else
# Détecter la forge depuis l'origin git du projet
DETECTED_GITEA_URL=""
IS_GITHUB=false
if command -v git >/dev/null && git -C "$PROJECT_DIR" rev-parse --git-dir &>/dev/null; then
ORIGIN_URL=$(git -C "$PROJECT_DIR" remote get-url origin 2>/dev/null || true)
if [[ -n "$ORIGIN_URL" ]]; then
# Extraire scheme + host (supporte https://host/... et git@host:...)
if [[ "$ORIGIN_URL" =~ ^(https?://[^/]+) ]]; then
ORIGIN_HOST="${BASH_REMATCH[1]}"
elif [[ "$ORIGIN_URL" =~ ^git@([^:]+): ]]; then
ORIGIN_HOST="https://${BASH_REMATCH[1]}"
fi
if [[ "${ORIGIN_HOST:-}" == *"github.com"* ]]; then
IS_GITHUB=true
info "Forge détectée : GitHub"
elif [[ -n "${ORIGIN_HOST:-}" ]]; then
# Tester si c'est un Gitea/Forgejo via son API
if curl -fsSL --max-time 5 "${ORIGIN_HOST}/api/v1/version" 2>/dev/null | grep -q '"version"'; then
DETECTED_GITEA_URL="$ORIGIN_HOST"
info "Forge détectée : Gitea/Forgejo ($DETECTED_GITEA_URL)"
else
warning "Forge non reconnue pour $ORIGIN_HOST (ni GitHub ni Gitea/Forgejo)"
fi
fi
fi
fi
{
echo "# Secrets du devcontainer — ne pas committer"
echo ""
if $IS_GITHUB; then
echo "# GitHub"
echo "GITHUB_TOKEN="
else
echo "# GitHub (non détecté pour ce projet)"
echo "# GITHUB_TOKEN="
fi
echo ""
if [[ -n "$DETECTED_GITEA_URL" ]]; then
echo "# Gitea"
echo "GITEA_URL=${DETECTED_GITEA_URL}"
echo "GITEA_TOKEN="
else
echo "# Gitea (non détecté pour ce projet)"
echo "# GITEA_URL="
echo "# GITEA_TOKEN="
fi
} > "$ENV_FILE"
info "Created .env"
fi
# --- .gitignore ---
GITIGNORE="$PROJECT_DIR/.gitignore"
if [[ -f "$GITIGNORE" ]]; then
if grep -q "\.devcontainer/config/" "$GITIGNORE"; then
warning ".gitignore already contains .devcontainer/config/, skipping"
else
printf '\n# PMOCrazyCoder devcontainer runtime config\n.devcontainer/config/\n.devcontainer/.env\n' >> "$GITIGNORE"
info "Updated .gitignore"
fi
else
printf '# PMOCrazyCoder devcontainer runtime config\n.devcontainer/config/\n.devcontainer/.env\n' > "$GITIGNORE"
info "Created .gitignore"
fi
echo
info "Done. Next steps:"
echo " 1. Renseigner les tokens dans .devcontainer/.env"
echo " 2. Open the project in VS Code / Zed / Cursor"
echo " 3. Reopen in container (the image will be pulled from the registry)"
echo " 4. Run /login in the Claude Agent panel to authenticate"
echo
info "To use a different local LLM API endpoint:"
echo " LOCAL_LLM_API=http://myserver:1248/v1 bash setup-devcontainer.sh"

View File

@@ -22,9 +22,8 @@ def additionner(a: float, b: float) -> float:
@mcp.tool @mcp.tool
async def increment(ctx: Context) -> tuple[int, str]: async def increment(ctx: Context) -> tuple[int, str]:
count = ctx.get_state("count") or 0 count = await ctx.get_state("count") or 0
ctx.set_state("count", count + 1) return int(count) + 1, ctx.session_id
return count + 1, ctx.session_id
# Lancement du serveur MCP # Lancement du serveur MCP

View File

@@ -1,2 +1 @@
from .jj import * from .jj import *
from .sandbox import *

View File

@@ -1,10 +1,9 @@
from crazy_mcp import Context, mcp from crazy_mcp import Context, mcp
from jujutsu import jj_log from jujutsu import jj_log, jj_new
from sandbox import get_active_project, get_project_path
@mcp.tool(description="Get the commit history for a project") @mcp.tool(description="Get the commit history for a project")
def jj_log_tool(project_name: str | None = None, ctx: Context = None) -> dict: def jj_log_tool(project_name: str | None = None, ctx: Context | None = None) -> dict:
""" """
Get the commit history for a project. Get the commit history for a project.
@@ -15,19 +14,70 @@ def jj_log_tool(project_name: str | None = None, ctx: Context = None) -> dict:
Returns: Returns:
dict: The commit history as JSON structure dict: The commit history as JSON structure
""" """
# If no project name is provided, try to get the active project
if project_name is None: if project_name is None:
if ctx is None: if ctx is None:
raise ValueError("ctx is required when project_name is None") raise ValueError("ctx is required when project_name is None")
project_info = get_active_project(ctx.session_id) project_info = ctx.get_state("active_project")
if project_info is None: if project_info is None:
raise ValueError("No active project set and no project name provided") raise ValueError("No active project set and no project name provided")
project_path = project_info[1] project_path = project_info[1] if isinstance(project_info, tuple) else None
else:
project_path = get_project_path(project_name)
if project_path is None: if project_path is None:
raise ValueError(f"Project '{project_name}' not found") raise ValueError("No project path found in active project")
else:
sandbox_root = "/sandbox"
project_path = f"{sandbox_root}/{project_name}"
# Call the jj_log function from jujutsu module
return jj_log(project_path) return jj_log(project_path)
@mcp.tool(description="Create a new commit with AI-generated description")
def jj_new_tool(
project_name: str | None = None,
revisions: list[str] | None = None,
message: str | None = None,
no_edit: bool = False,
insert_after: str | None = None,
insert_before: str | None = None,
options: list[str] | None = None,
ctx: Context | None = None,
) -> str:
"""
Create a new commit after documenting the current change with AI.
Args:
project_name (str, optional): The name of the project. If None, uses the active project.
revisions (list[str], optional): List of parent revisions (default: [@])
message (str, optional): The change description to use
no_edit (bool): Do not edit the newly created change
insert_after (str, optional): Insert the new change after the given commit(s)
insert_before (str, optional): Insert the new change before the given commit(s)
options (list[str], optional): List of additional options
ctx (Context, optional): The MCP context object containing session information
Returns:
str: Output of jj new command
"""
if project_name is None:
if ctx is None:
raise ValueError("ctx is required when project_name is None")
project_info = ctx.get_state("active_project")
if project_info is None:
raise ValueError("No active project set and no project name provided")
project_path = project_info[1] if isinstance(project_info, tuple) else None
if project_path is None:
raise ValueError("No project path found in active project")
else:
sandbox_root = "/sandbox"
project_path = f"{sandbox_root}/{project_name}"
return jj_new(
revisions=revisions,
message=message,
no_edit=no_edit,
insert_after=insert_after,
insert_before=insert_before,
options=options,
working_dir=project_path,
)

View File

@@ -1,89 +0,0 @@
from crazy_mcp import Context, mcp
from sandbox import (
get_active_project,
install_project,
remove_project,
sandboxed_project_list,
set_active_project,
)
@mcp.tool(
description="Clone a Git repository into the sandbox. Note: Access to the cloned project is only possible through MCP tools of this server. WARNING: This operation creates a sandboxed environment that prevents direct filesystem access to the project contents. Any attempt to access project files outside of the MCP tools will be blocked by security restrictions."
)
def install_project_tool(url: str, project_name: str = None) -> str:
"""
Install a Git project into the sandbox environment.
Args:
url: The Git repository URL to clone
project_name: Optional project name (if None, derived from URL)
Returns:
str: Confirmation message with project name
"""
project_name = install_project(url, project_name)
return f"Project '{project_name}' installed successfully in sandbox"
@mcp.tool(
description="Remove a project from the sandbox. Note: Access to the cloned project is only possible through MCP tools of this server. WARNING: This operation removes the entire project sandboxed environment and all associated security restrictions will be enforced."
)
def remove_project_tool(project_name: str) -> str:
"""
Remove a project from the sandbox environment.
Args:
project_name (str): The name of the project to remove
Returns:
str: Confirmation message with project name
"""
success = remove_project(project_name)
if success:
return f"Project '{project_name}' removed successfully from sandbox"
else:
return f"Failed to remove project '{project_name}' from sandbox"
@mcp.tool(
description="List all projects available in the sandbox. Note: Direct filesystem access to project contents is forbidden. All project interactions must be done through MCP tools."
)
def list_sandboxed_projects() -> list:
"""
Get a list of all projects available in the sandbox.
Returns:
list: A list of project names
"""
return sandboxed_project_list()
@mcp.tool(description="Set the active project for a given session ID")
def set_active_project_tool(project_name: str, ctx: Context) -> bool:
"""
Set the active project for a given session ID.
Args:
project_name (str): The name of the project to set as active
ctx (Context): The MCP context object containing session information
Returns:
bool: True if successful, False if the project doesn't exist
"""
return set_active_project(ctx.session_id, project_name)
@mcp.tool(description="Get the active project for a given session ID")
def get_active_project_tool(ctx: Context) -> tuple[str, str] | None:
"""
Get the active project for a given session ID.
Args:
ctx (Context): The MCP context object containing session information
Returns:
tuple: (project_name, project_path) if session exists, None otherwise
"""
project_info = get_active_project(ctx.session_id)
return project_info

View File

@@ -1,12 +1,13 @@
import json import json
import re import os
import urllib.error
from ollama import prompt import urllib.request
from typing import Dict
def structure_to_json(text, schema): def structure_to_json(text: str, schema: Dict) -> Dict:
""" """
Convert structured text to JSON using Ollama. Convert structured text to JSON using LOCAL_LLM_API.
Args: Args:
text (str): The structured text to convert text (str): The structured text to convert
@@ -15,37 +16,61 @@ def structure_to_json(text, schema):
Returns: Returns:
dict: The parsed JSON object dict: The parsed JSON object
""" """
# Get the API URL from environment variable
local_llm_api = os.environ.get(
"LOCAL_LLM_API", "http://host.docker.internal:1248/v1"
)
# Format the schema as JSON string # Format the schema as JSON string
schema_json = json.dumps(schema, indent=2) schema_json = json.dumps(schema, indent=2)
# Create the prompt following the pattern from your bash command # Create the prompt following the pattern from jj-ai-commit.sh
full_prompt = f"""Transform the following structured text into JSON according to the given schema. prompt_text = f"""Analyse this text and respond ONLY with a valid JSON object matching the schema.
**Return only valid JSON, without any Markdown formatting :** Text:
- no backticks,
- no code blocks,
- no extra text
Input text:
{text} {text}
Schema: Schema:
{schema_json}""" {schema_json}"""
# Send to Ollama # Build the request body
response = prompt("qwen3:0.6b", full_prompt, allow_pull=True) request_body = {
"model": "qwen3-coder",
"messages": [{"role": "user", "content": prompt_text}],
}
# Remove any markdown formatting (backticks, code blocks) that Ollama might include # Build the URL
# Only remove backticks at the beginning or end of the response, not those within the JSON url = f"{local_llm_api}/chat/completions"
clean_response = response.strip()
while clean_response.startswith("`") or clean_response.startswith("```"): # Create the request
clean_response = clean_response[1:].strip() data = json.dumps(request_body).encode("utf-8")
while clean_response.endswith("`") or clean_response.endswith("```"): req = urllib.request.Request(
clean_response = clean_response[:-1].strip() url, data=data, headers={"Content-Type": "application/json"}, method="POST"
)
# Try to parse the response as JSON
try: try:
return json.loads(clean_response) # Execute the request
except json.JSONDecodeError: with urllib.request.urlopen(req) as response:
# If parsing fails, return the raw response result = json.loads(response.read().decode("utf-8"))
return {"error": "Failed to parse JSON", "raw_response": clean_response} except urllib.error.URLError as e:
return {"error": f"API request failed: {e}", "raw_response": ""}
except json.JSONDecodeError as e:
return {"error": f"Failed to parse response: {e}", "raw_response": ""}
try:
content = result.get("choices", [{}])[0].get("message", {}).get("content", "")
# Clean markdown backticks from response
clean_content = content.strip()
while clean_content.startswith("`") or clean_content.startswith("```"):
clean_content = clean_content[1:].strip()
while clean_content.endswith("`") or clean_content.endswith("```"):
clean_content = clean_content[:-1].strip()
# Parse the JSON content
return json.loads(clean_content)
except (json.JSONDecodeError, KeyError, IndexError) as e:
return {
"error": f"Failed to parse JSON: {e}",
"raw_response": json.dumps(result),
}

View File

@@ -0,0 +1,145 @@
import json
import os
import sys
import unittest
from unittest.mock import MagicMock, patch
sys.path.insert(0, "/workspace/src")
from json_extract import structure_to_json
class TestStructureToJson(unittest.TestCase):
def setUp(self):
os.environ.pop("LOCAL_LLM_API", None)
@patch("json_extract.urllib.request.urlopen")
def test_successful_conversion(self, mock_urlopen):
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(
{"choices": [{"message": {"content": '{"name": "John", "age": 30}'}}]}
).encode("utf-8")
mock_urlopen.return_value.__enter__.return_value = mock_response
text = "Nom: John, Age: 30"
schema = {
"type": "object",
"properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
}
result = structure_to_json(text, schema)
self.assertEqual(result["name"], "John")
self.assertEqual(result["age"], 30)
self.assertNotIn("error", result)
@patch("json_extract.urllib.request.urlopen")
def test_markdown_backtick_removal(self, mock_urlopen):
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(
{"choices": [{"message": {"content": '{"name": "John"}'}}]}
).encode("utf-8")
mock_urlopen.return_value.__enter__.return_value = mock_response
text = "Nom: John"
schema = {"type": "object", "properties": {"name": {"type": "string"}}}
result = structure_to_json(text, schema)
self.assertEqual(result["name"], "John")
@patch("json_extract.urllib.request.urlopen")
def test_api_error_handling(self, mock_urlopen):
import urllib.error
mock_urlopen.side_effect = urllib.error.URLError("Connection refused")
text = "Some text"
schema = {"type": "object"}
result = structure_to_json(text, schema)
self.assertIn("error", result)
self.assertIn("API request failed", result["error"])
@patch("json_extract.urllib.request.urlopen")
def test_invalid_json_response(self, mock_urlopen):
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(
{"choices": [{"message": {"content": "This is not JSON"}}]}
).encode("utf-8")
mock_urlopen.return_value.__enter__.return_value = mock_response
text = "Some text"
schema = {"type": "object"}
result = structure_to_json(text, schema)
self.assertIn("error", result)
self.assertIn("Failed to parse JSON", result["error"])
@patch("json_extract.urllib.request.urlopen")
def test_empty_response(self, mock_urlopen):
mock_response = MagicMock()
mock_response.read.return_value = json.dumps({"choices": [{}]}).encode("utf-8")
mock_urlopen.return_value.__enter__.return_value = mock_response
text = "Some text"
schema = {"type": "object"}
result = structure_to_json(text, schema)
self.assertIn("error", result)
@patch("json_extract.urllib.request.Request")
@patch("json_extract.urllib.request.urlopen")
def test_custom_api_url(self, mock_urlopen, mock_request):
os.environ["LOCAL_LLM_API"] = "http://custom-api:8080/v1"
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(
{"choices": [{"message": {"content": '{"data": "test"}'}}]}
).encode("utf-8")
mock_urlopen.return_value.__enter__.return_value = mock_response
text = "Test data"
schema = {"type": "object", "properties": {"data": {"type": "string"}}}
result = structure_to_json(text, schema)
self.assertEqual(result["data"], "test")
mock_request.assert_called_once()
call_args = mock_request.call_args
self.assertEqual(call_args[0][0], "http://custom-api:8080/v1/chat/completions")
@patch("json_extract.urllib.request.Request")
@patch("json_extract.urllib.request.urlopen")
def test_default_api_url(self, mock_urlopen, mock_request):
os.environ.pop("LOCAL_LLM_API", None)
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(
{"choices": [{"message": {"content": '{"data": "test"}'}}]}
).encode("utf-8")
mock_urlopen.return_value.__enter__.return_value = mock_response
text = "Test data"
schema = {"type": "object"}
result = structure_to_json(text, schema)
self.assertNotIn("error", result)
mock_request.assert_called_once()
call_args = mock_request.call_args
self.assertEqual(
call_args[0][0], "http://host.docker.internal:1248/v1/chat/completions"
)
def test_empty_text_and_schema(self):
result = structure_to_json("", {})
self.assertIn("error", result)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,8 +1,9 @@
import os from typing import Optional
from sys import implementation
from json_extract import structure_to_json from json_extract import structure_to_json
from jujutsu.command import jj_diff
from jujutsu.command import jj_log as jj_log_command from jujutsu.command import jj_log as jj_log_command
from jujutsu.command import jj_new as jj_new_command
from .command import jj from .command import jj
@@ -37,3 +38,95 @@ def jj_log(working_dir=None):
# Convert the text output to JSON using the schema # Convert the text output to JSON using the schema
return structure_to_json(log_output, schema) return structure_to_json(log_output, schema)
def jj_commit_message(working_dir=None):
"""
Generate a commit message from the current diff using LOCAL_LLM_API.
Args:
working_dir: The path of the project (optional)
Returns:
str: Generated commit message in format "title\n\nmessage"
"""
diff_output = jj_diff(working_dir=working_dir)
prompt = """Analyse ce diff et réponds UNIQUEMENT avec un objet JSON valide contenant "title" (string) et "message" (string) pour le message de commit. Pas de texte avant ou après le JSON."""
content = f"{prompt}\n{diff_output}"
schema = {
"type": "object",
"properties": {
"title": {"type": "string"},
"message": {"type": "string"},
},
"required": ["title", "message"],
}
result = structure_to_json(content, schema)
if "error" in result:
return result["error"]
title = result.get("title", "")
message = result.get("message", "")
return f"{title}\n\n{message}"
def jj_auto_describe(working_dir=None):
"""
Automatically describe the current change using jj describe with AI-generated message.
Args:
working_dir: The path of the project (optional)
Returns:
str: Output of jj describe command
"""
message = jj_commit_message(working_dir=working_dir)
args = ["-m", message]
return jj("describe", args=args, working_dir=working_dir)
def jj_new(
revisions=None,
message=None,
no_edit=False,
insert_after=None,
insert_before=None,
options=None,
working_dir: Optional[str] = None,
):
"""
Create a new commit after documenting the current change with AI.
First runs jj_auto_describe to document the current working copy,
then creates a new empty commit with jj new.
Args:
revisions: List of parent revisions (default: [@])
message: The change description to use
no_edit: Do not edit the newly created change
insert_after: Insert the new change after the given commit(s)
insert_before: Insert the new change before the given commit(s)
options: List of additional options
working_dir: The path of the project (optional)
Returns:
str: Output of jj new command
"""
jj_auto_describe(working_dir=working_dir)
return jj_new_command(
revisions=revisions,
message=message,
no_edit=no_edit,
insert_after=insert_after,
insert_before=insert_before,
options=options,
working_dir=working_dir,
)

View File

@@ -1,4 +1,3 @@
import json
import os import os
import subprocess import subprocess
from typing import List, Optional from typing import List, Optional
@@ -83,21 +82,99 @@ def jj_clone(url, destination=None, options=None, working_dir: Optional[str] = N
Returns: Returns:
str: Output of the jj git clone command str: Output of the jj git clone command
""" """
# For jj git clone, we build the full command and pass it to jj function args = ["clone", url]
# The subcommand "git clone" is handled by jj itself
cmd_args = ["git", "clone", url]
if destination: if destination:
cmd_args.append(destination) args.append(destination)
if options: if options:
cmd_args.extend(options) args.extend(options)
# Call the main jj function with subcommand "git clone" return jj("git", args=args, working_dir=working_dir)
return jj(
"git",
options=["clone", url], def jj_describe(
args=destination, message: Optional[str] = None, options=None, working_dir: Optional[str] = None
template=None, ) -> str:
working_dir=working_dir, """
) Execute jj describe command to set or view commit description.
Args:
message: The description text to set (optional, reads current if not provided)
options: List of additional options for jj describe
working_dir: Working directory for the command (optional)
Returns:
str: Output of the jj describe command
"""
args = []
if message:
args.append(message)
if options:
args.extend(options)
return jj("describe", args=args, working_dir=working_dir)
def jj_diff(
options=None, args=None, template=None, working_dir: Optional[str] = None
) -> str:
"""
Execute jj diff command with optional arguments.
Args:
options: List of options to pass to jj diff
args: List of additional arguments
template: Template to use
working_dir: Working directory for the command (optional)
Returns:
str: Output of the jj diff command
"""
return jj("diff", options, args, template, working_dir=working_dir)
def jj_new(
revisions=None,
message=None,
no_edit=False,
insert_after=None,
insert_before=None,
options=None,
working_dir: Optional[str] = None,
) -> str:
"""
Execute jj new command to create a new empty change.
Args:
revisions: List of parent revisions (default: [@])
message: The change description to use
no_edit: Do not edit the newly created change
insert_after: Insert the new change after the given commit(s)
insert_before: Insert the new change before the given commit(s)
options: List of additional options
working_dir: Working directory for the command (optional)
Returns:
str: Output of the jj new command
"""
args = []
if revisions:
args.extend(revisions)
if message:
args.extend(["-m", message])
if no_edit:
args.append("--no-edit")
if insert_after:
args.extend(["--insert-after", insert_after])
if insert_before:
args.extend(["--insert-before", insert_before])
if options:
args.extend(options)
return jj("new", args=args, working_dir=working_dir)

View File

@@ -1,83 +0,0 @@
import os
import requests
# Get Ollama server address from environment variable, default to local address
ollama_addr = os.environ.get("OLLAMA_ADDR", "http://host.docker.internal:11434")
def prompt(model, text, allow_pull=False):
"""
Send a prompt to Ollama server and return the response.
Args:
model (str): The model to use
text (str): The prompt text to send
allow_pull (bool): If True, automatically download the model if not available
Returns:
str: The response from the Ollama server
"""
# Check if model is available
if allow_pull and not is_model_available(model):
# Try to pull the model
pull_result = pull_model(model)
if not pull_result.startswith("Model downloaded successfully"):
return pull_result # Return error from pull
url = f"{ollama_addr}/api/generate"
payload = {"model": model, "prompt": text, "stream": False}
try:
response = requests.post(url, json=payload)
response.raise_for_status()
return response.json()["response"]
except requests.exceptions.RequestException as e:
return f"Error: {str(e)}"
def pull_model(model_name, force=False):
"""
Download a model from Ollama.
Args:
model_name (str): The name of the model to download
force (bool): If True, forces download even if model already exists
Returns:
str: Success message or error message
"""
url = f"{ollama_addr}/api/pull"
payload = {"name": model_name}
if force:
payload["force"] = True
try:
response = requests.post(url, json=payload)
response.raise_for_status()
return "Model downloaded successfully"
except requests.exceptions.RequestException as e:
return f"Error downloading model: {str(e)}"
def is_model_available(model_name):
"""
Check if a model is available in Ollama.
Args:
model_name (str): The name of the model to check
Returns:
bool: True if model is available, False otherwise
"""
url = f"{ollama_addr}/api/tags"
try:
response = requests.get(url)
response.raise_for_status()
models = response.json().get("models", [])
return any(model.get("name") == model_name for model in models)
except requests.exceptions.RequestException:
return False

View File

@@ -1,139 +0,0 @@
import os
import re
import shutil
import subprocess
from pathlib import Path
from jujutsu.command import jj_clone
__sandbox_root__ = "/sandbox"
# Create the sandbox root directory if it doesn't exist
os.makedirs(__sandbox_root__, exist_ok=True)
# Dictionary to store session information (session_id -> (project_name, project_path))
__session_projects__ = {}
def get_sandbox_root():
"""Return the sandbox root directory path."""
return __sandbox_root__
def install_project(url, project_name=None):
"""
Clone a Git project into the sandbox root directory.
Args:
url: The Git repository URL to clone
project_name: Optional project name (if None, derived from URL)
Returns:
str: The name of the cloned project
"""
# If no project name is provided, derive it from the URL
if project_name is None:
# Extract project name from URL (e.g., from 'https://github.com/user/repo.git' get 'repo')
project_name = re.sub(r".*/([^/]+?)(?:\.git)?$", r"\1", url)
# Clone the repository using the working_dir parameter to ensure it's cloned in the sandbox
jj_clone(url, working_dir=__sandbox_root__)
return project_name
def remove_project(project_name):
"""
Remove a project from the sandbox.
Args:
project_name (str): The name of the project to remove
Returns:
bool: True if the project was removed successfully, False otherwise
"""
project_path = os.path.join(__sandbox_root__, project_name)
try:
if os.path.exists(project_path):
shutil.rmtree(project_path)
return True
return False
except (OSError, PermissionError):
return False
def sandboxed_project_list():
"""
List all projects available in the sandbox.
Returns:
list: A list of project names (strings)
"""
projects = []
try:
with os.scandir(__sandbox_root__) as entries:
for entry in entries:
if entry.is_dir():
projects.append(entry.name)
except (OSError, PermissionError):
# Return empty list if we can't access the directory
pass
return projects
def set_active_project(session_id, project_name):
"""
Set the active project for a given session ID.
Args:
session_id (str): The session identifier
project_name (str): The name of the project to set as active
Returns:
bool: True if successful, False if the project doesn't exist
"""
# Check if the project exists
projects = sandboxed_project_list()
if project_name not in projects:
return False
# Get the project path
project_path = os.path.join(__sandbox_root__, project_name)
# Store the project information in the session dictionary
__session_projects__[session_id] = (project_name, project_path)
return True
def get_project_path(project_name):
"""
Get the path of a project on the disk.
Args:
project_name (str): The name of the project
Returns:
str: The full path to the project directory
"""
return os.path.join(__sandbox_root__, project_name)
def get_active_project(session_id):
"""
Get the active project for a given session ID.
Args:
session_id (str): The session identifier
Returns:
tuple: (project_name, project_path) if session exists, None otherwise
"""
# Check if the session ID exists
if session_id not in __session_projects__:
return None
# Return the project information
return __session_projects__[session_id]

View File

@@ -1,7 +0,0 @@
import requests
url = "http://localhost:6666" # point d'entrée principal
payload = {"tool": "echo", "input": {"text": "Bonjour MCP!"}}
resp = requests.post(url, json=payload)
print(resp.json()) # {"echo": "Bonjour MCP!"} attendu

225
toto.diff Normal file
View File

@@ -0,0 +1,225 @@
diff --git a/bin/jj-ai-commit.sh b/bin/jj-ai-commit.sh
new file mode 100755
index 0000000000..b89444b55d
--- /dev/null
+++ b/bin/jj-ai-commit.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [ -z "${LOCAL_LLM_API:-}" ]; then
+ echo "Error: LOCAL_LLM_API environment variable is not set."
+ exit 1
+fi
+
+MODEL="qwen/qwen3-coder-next"
+
+# Capture seulement les lignes modifiées pour limiter la taille
+diff="$(jj diff --git | grep '^[+-]' | head -n 500)"
+
+request_json=$(jq -n \
+ --arg model "$MODEL" \
+ --arg diff "$diff" \
+'{
+ model: $model,
+ temperature: 0.2,
+ response_format: { type: "json_object" },
+ messages: [
+ {
+ role: "user",
+ content: (
+ "Analyse ce diff et réponds UNIQUEMENT avec un objet JSON valide contenant \"title\" (string) et \"message\" (string). Pas de texte avant ou après le JSON.\n\n" + $diff
+ )
+ }
+ ]
+}')
+
+response=$(curl -s "$LOCAL_LLM_API/chat/completions" \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer dummy" \
+ -d "$request_json")
+
+# Récupérer et formater le commit message
+echo "$response" | jq -r '.choices[0].message.content' | jq -r '.title + "\n\n" + .message'
diff --git a/pyproject.toml b/pyproject.toml
index 4d15816f39..cd40d11e6a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,6 @@
description = "A crazy MCP server"
requires-python = ">=3.10"
dependencies = [
- "crazy-mcp>=0.1.0",
]
[project.optional-dependencies]
diff --git a/src/PMOCrazyCoder.py b/src/PMOCrazyCoder.py
index 4a10aa6427..fd9bf4a774 100644
--- a/src/PMOCrazyCoder.py
+++ b/src/PMOCrazyCoder.py
@@ -22,9 +22,8 @@
@mcp.tool
async def increment(ctx: Context) -> tuple[int, str]:
- count = ctx.get_state("count") or 0
- ctx.set_state("count", count + 1)
- return count + 1, ctx.session_id
+ count = await ctx.get_state("count") or 0
+ return int(count) + 1, ctx.session_id
# Lancement du serveur MCP
diff --git a/src/crazy_mcp/__init__.py b/src/crazy_mcp/__init__.py
new file mode 100644
index 0000000000..bda4dfa2ca
--- /dev/null
+++ b/src/crazy_mcp/__init__.py
@@ -0,0 +1,3 @@
+from fastmcp import Context, FastMCP
+
+mcp = FastMCP("PMO Crazy coder")
diff --git a/src/crazy_mcp/tools/__init__.py b/src/crazy_mcp/tools/__init__.py
new file mode 100644
index 0000000000..23a3da5bd9
--- /dev/null
+++ b/src/crazy_mcp/tools/__init__.py
@@ -0,0 +1,2 @@
+from .jj import *
+from .sandbox import *
diff --git a/src/crazy_mcp/tools/jj.py b/src/crazy_mcp/tools/jj.py
new file mode 100644
index 0000000000..f6bbed95c8
--- /dev/null
+++ b/src/crazy_mcp/tools/jj.py
@@ -0,0 +1,33 @@
+from crazy_mcp import Context, mcp
+from jujutsu import jj_log
+from sandbox import get_active_project, get_project_path
+
+
+@mcp.tool(description="Get the commit history for a project")
+def jj_log_tool(project_name: str | None = None, ctx: Context = None) -> dict:
+ """
+ Get the commit history for a project.
+
+ Args:
+ project_name (str, optional): The name of the project. If None, uses the active project.
+ ctx (Context): The MCP context object containing session information
+
+ Returns:
+ dict: The commit history as JSON structure
+ """
+ # If no project name is provided, try to get the active project
+ if project_name is None:
+ if ctx is None:
+ raise ValueError("ctx is required when project_name is None")
+
+ project_info = get_active_project(ctx.session_id)
+ if project_info is None:
+ raise ValueError("No active project set and no project name provided")
+ project_path = project_info[1]
+ else:
+ project_path = get_project_path(project_name)
+ if project_path is None:
+ raise ValueError(f"Project '{project_name}' not found")
+
+ # Call the jj_log function from jujutsu module
+ return jj_log(project_path)
diff --git a/src/crazy_mcp/tools/sandbox.py b/src/crazy_mcp/tools/sandbox.py
new file mode 100644
index 0000000000..68da135db1
--- /dev/null
+++ b/src/crazy_mcp/tools/sandbox.py
@@ -0,0 +1,89 @@
+from crazy_mcp import Context, mcp
+from sandbox import (
+ get_active_project,
+ install_project,
+ remove_project,
+ sandboxed_project_list,
+ set_active_project,
+)
+
+
+@mcp.tool(
+ description="Clone a Git repository into the sandbox. Note: Access to the cloned project is only possible through MCP tools of this server. WARNING: This operation creates a sandboxed environment that prevents direct filesystem access to the project contents. Any attempt to access project files outside of the MCP tools will be blocked by security restrictions."
+)
+def install_project_tool(url: str, project_name: str = None) -> str:
+ """
+ Install a Git project into the sandbox environment.
+
+ Args:
+ url: The Git repository URL to clone
+ project_name: Optional project name (if None, derived from URL)
+
+ Returns:
+ str: Confirmation message with project name
+ """
+ project_name = install_project(url, project_name)
+ return f"Project '{project_name}' installed successfully in sandbox"
+
+
+@mcp.tool(
+ description="Remove a project from the sandbox. Note: Access to the cloned project is only possible through MCP tools of this server. WARNING: This operation removes the entire project sandboxed environment and all associated security restrictions will be enforced."
+)
+def remove_project_tool(project_name: str) -> str:
+ """
+ Remove a project from the sandbox environment.
+
+ Args:
+ project_name (str): The name of the project to remove
+
+ Returns:
+ str: Confirmation message with project name
+ """
+ success = remove_project(project_name)
+ if success:
+ return f"Project '{project_name}' removed successfully from sandbox"
+ else:
+ return f"Failed to remove project '{project_name}' from sandbox"
+
+
+@mcp.tool(
+ description="List all projects available in the sandbox. Note: Direct filesystem access to project contents is forbidden. All project interactions must be done through MCP tools."
+)
+def list_sandboxed_projects() -> list:
+ """
+ Get a list of all projects available in the sandbox.
+
+ Returns:
+ list: A list of project names
+ """
+ return sandboxed_project_list()
+
+
+@mcp.tool(description="Set the active project for a given session ID")
+def set_active_project_tool(project_name: str, ctx: Context) -> bool:
+ """
+ Set the active project for a given session ID.
+
+ Args:
+ project_name (str): The name of the project to set as active
+ ctx (Context): The MCP context object containing session information
+
+ Returns:
+ bool: True if successful, False if the project doesn't exist
+ """
+ return set_active_project(ctx.session_id, project_name)
+
+
+@mcp.tool(description="Get the active project for a given session ID")
+def get_active_project_tool(ctx: Context) -> tuple[str, str] | None:
+ """
+ Get the active project for a given session ID.
+
+ Args:
+ ctx (Context): The MCP context object containing session information
+
+ Returns:
+ tuple: (project_name, project_path) if session exists, None otherwise
+ """
+ project_info = get_active_project(ctx.session_id)
+ return project_info
diff --git a/toto.diff b/toto.diff
new file mode 100644
index 0000000000..e69de29bb2

9
zed-integration/mcp.json Normal file
View File

@@ -0,0 +1,9 @@
{
"mcpServers": {
"pmocrazy-coder": {
"command": "python3",
"args": ["opt/dev-tools/list_imports.py"],
"cwd": "."
}
}
}