push-oynptmmzrplt #2
95
.claude-devcontainer/.claude.json
Normal file
95
.claude-devcontainer/.claude.json
Normal 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"
|
||||||
|
}
|
||||||
1
.claude-devcontainer/.credentials.json
Normal file
1
.claude-devcontainer/.credentials.json
Normal 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"}}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"firstStartTime": "2026-03-11T12:56:06.706Z"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
2186
.claude-devcontainer/cache/changelog.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
17
.claude-devcontainer/plugins/blocklist.json
Normal file
17
.claude-devcontainer/plugins/blocklist.json
Normal 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
@@ -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"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"agentType":"Explore"}
|
||||||
@@ -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
|
||||||
9
.devcontainer/.env.example
Normal file
9
.devcontainer/.env.example
Normal 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
|
||||||
14
.devcontainer/devcontainer.json
Normal file
14
.devcontainer/devcontainer.json
Normal 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",
|
||||||
|
}
|
||||||
28
.devcontainer/docker-compose.yml
Normal file
28
.devcontainer/docker-compose.yml
Normal 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
5
.gitignore
vendored
@@ -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
8
.opencode/config.json
Normal 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
28
.opencode/instructions.md
Normal 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.
|
||||||
@@ -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",
|
|
||||||
}
|
}
|
||||||
|
|||||||
153
Dockerfile
153
Dockerfile
@@ -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
190
Makefile
@@ -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/^## //'
|
||||||
|
|||||||
@@ -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
197
README.md
@@ -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
191
bin/crazycoder-setup.sh
Normal 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
48
bin/jj-ai-commit.sh
Executable 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
17
docker-entrypoint.sh
Executable 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
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
49
opencode/AGENTs.md
Normal 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
32
pyproject.toml
Normal 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
169
setup-devcontainer.sh
Executable 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"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
from .jj import *
|
from .jj import *
|
||||||
from .sandbox import *
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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),
|
||||||
|
}
|
||||||
|
|||||||
145
src/json_extract/test_json_extract.py
Normal file
145
src/json_extract/test_json_extract.py
Normal 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()
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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]
|
|
||||||
7
test.py
7
test.py
@@ -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
225
toto.diff
Normal 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
9
zed-integration/mcp.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"pmocrazy-coder": {
|
||||||
|
"command": "python3",
|
||||||
|
"args": ["opt/dev-tools/list_imports.py"],
|
||||||
|
"cwd": "."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user