diff --git a/.claude/skills/gitnexus/gitnexus-cli/SKILL.md b/.claude/skills/gitnexus/gitnexus-cli/SKILL.md new file mode 100644 index 0000000..a10104a --- /dev/null +++ b/.claude/skills/gitnexus/gitnexus-cli/SKILL.md @@ -0,0 +1,83 @@ +--- +name: gitnexus-cli +description: "Use when the user needs to run GitNexus CLI commands like analyze/index a repo, check status, clean the index, generate a wiki, or list indexed repos. Examples: \"Index this repo\", \"Reanalyze the codebase\", \"Generate a wiki\"" +--- + +# GitNexus CLI Commands + +All commands work via `npx` — no global install required. + +## Commands + +### analyze — Build or refresh the index + +```bash +npx gitnexus analyze +``` + +Run from the project root. This parses all source files, builds the knowledge graph, writes it to `.gitnexus/`, and generates CLAUDE.md / AGENTS.md context files. + +| Flag | Effect | +| -------------- | ---------------------------------------------------------------- | +| `--force` | Force full re-index even if up to date | +| `--embeddings` | Enable embedding generation for semantic search (off by default) | +| `--drop-embeddings` | Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` preserves them. | + +**When to run:** First time in a project, after major code changes, or when `gitnexus://repo/{name}/context` reports the index is stale. In Claude Code, a PostToolUse hook runs `analyze` automatically after `git commit` and `git merge`, preserving embeddings if previously generated. + +### status — Check index freshness + +```bash +npx gitnexus status +``` + +Shows whether the current repo has a GitNexus index, when it was last updated, and symbol/relationship counts. Use this to check if re-indexing is needed. + +### clean — Delete the index + +```bash +npx gitnexus clean +``` + +Deletes the `.gitnexus/` directory and unregisters the repo from the global registry. Use before re-indexing if the index is corrupt or after removing GitNexus from a project. + +| Flag | Effect | +| --------- | ------------------------------------------------- | +| `--force` | Skip confirmation prompt | +| `--all` | Clean all indexed repos, not just the current one | + +### wiki — Generate documentation from the graph + +```bash +npx gitnexus wiki +``` + +Generates repository documentation from the knowledge graph using an LLM. Requires an API key (saved to `~/.gitnexus/config.json` on first use). + +| Flag | Effect | +| ------------------- | ----------------------------------------- | +| `--force` | Force full regeneration | +| `--model ` | LLM model (default: minimax/minimax-m2.5) | +| `--base-url ` | LLM API base URL | +| `--api-key ` | LLM API key | +| `--concurrency ` | Parallel LLM calls (default: 3) | +| `--gist` | Publish wiki as a public GitHub Gist | + +### list — Show all indexed repos + +```bash +npx gitnexus list +``` + +Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_repos` tool provides the same information. + +## After Indexing + +1. **Read `gitnexus://repo/{name}/context`** to verify the index loaded +2. Use the other GitNexus skills (`exploring`, `debugging`, `impact-analysis`, `refactoring`) for your task + +## Troubleshooting + +- **"Not inside a git repository"**: Run from a directory inside a git repo +- **Index is stale after re-analyzing**: Restart Claude Code to reload the MCP server +- **Embeddings slow**: Omit `--embeddings` (it's off by default) or set `OPENAI_API_KEY` for faster API-based embedding diff --git a/.claude/skills/gitnexus/gitnexus-debugging/SKILL.md b/.claude/skills/gitnexus/gitnexus-debugging/SKILL.md new file mode 100644 index 0000000..9510b97 --- /dev/null +++ b/.claude/skills/gitnexus/gitnexus-debugging/SKILL.md @@ -0,0 +1,89 @@ +--- +name: gitnexus-debugging +description: "Use when the user is debugging a bug, tracing an error, or asking why something fails. Examples: \"Why is X failing?\", \"Where does this error come from?\", \"Trace this bug\"" +--- + +# Debugging with GitNexus + +## When to Use + +- "Why is this function failing?" +- "Trace where this error comes from" +- "Who calls this method?" +- "This endpoint returns 500" +- Investigating bugs, errors, or unexpected behavior + +## Workflow + +``` +1. gitnexus_query({query: ""}) → Find related execution flows +2. gitnexus_context({name: ""}) → See callers/callees/processes +3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow +4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed +``` + +> If "Index is stale" → run `npx gitnexus analyze` in terminal. + +## Checklist + +``` +- [ ] Understand the symptom (error message, unexpected behavior) +- [ ] gitnexus_query for error text or related code +- [ ] Identify the suspect function from returned processes +- [ ] gitnexus_context to see callers and callees +- [ ] Trace execution flow via process resource if applicable +- [ ] gitnexus_cypher for custom call chain traces if needed +- [ ] Read source files to confirm root cause +``` + +## Debugging Patterns + +| Symptom | GitNexus Approach | +| -------------------- | ---------------------------------------------------------- | +| Error message | `gitnexus_query` for error text → `context` on throw sites | +| Wrong return value | `context` on the function → trace callees for data flow | +| Intermittent failure | `context` → look for external calls, async deps | +| Performance issue | `context` → find symbols with many callers (hot paths) | +| Recent regression | `detect_changes` to see what your changes affect | + +## Tools + +**gitnexus_query** — find code related to error: + +``` +gitnexus_query({query: "payment validation error"}) +→ Processes: CheckoutFlow, ErrorHandling +→ Symbols: validatePayment, handlePaymentError, PaymentException +``` + +**gitnexus_context** — full context for a suspect: + +``` +gitnexus_context({name: "validatePayment"}) +→ Incoming calls: processCheckout, webhookHandler +→ Outgoing calls: verifyCard, fetchRates (external API!) +→ Processes: CheckoutFlow (step 3/7) +``` + +**gitnexus_cypher** — custom call chain traces: + +```cypher +MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"}) +RETURN [n IN nodes(path) | n.name] AS chain +``` + +## Example: "Payment endpoint returns 500 intermittently" + +``` +1. gitnexus_query({query: "payment error handling"}) + → Processes: CheckoutFlow, ErrorHandling + → Symbols: validatePayment, handlePaymentError + +2. gitnexus_context({name: "validatePayment"}) + → Outgoing calls: verifyCard, fetchRates (external API!) + +3. READ gitnexus://repo/my-app/process/CheckoutFlow + → Step 3: validatePayment → calls fetchRates (external) + +4. Root cause: fetchRates calls external API without proper timeout +``` diff --git a/.claude/skills/gitnexus/gitnexus-exploring/SKILL.md b/.claude/skills/gitnexus/gitnexus-exploring/SKILL.md new file mode 100644 index 0000000..927a4e4 --- /dev/null +++ b/.claude/skills/gitnexus/gitnexus-exploring/SKILL.md @@ -0,0 +1,78 @@ +--- +name: gitnexus-exploring +description: "Use when the user asks how code works, wants to understand architecture, trace execution flows, or explore unfamiliar parts of the codebase. Examples: \"How does X work?\", \"What calls this function?\", \"Show me the auth flow\"" +--- + +# Exploring Codebases with GitNexus + +## When to Use + +- "How does authentication work?" +- "What's the project structure?" +- "Show me the main components" +- "Where is the database logic?" +- Understanding code you haven't seen before + +## Workflow + +``` +1. READ gitnexus://repos → Discover indexed repos +2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness +3. gitnexus_query({query: ""}) → Find related execution flows +4. gitnexus_context({name: ""}) → Deep dive on specific symbol +5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow +``` + +> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal. + +## Checklist + +``` +- [ ] READ gitnexus://repo/{name}/context +- [ ] gitnexus_query for the concept you want to understand +- [ ] Review returned processes (execution flows) +- [ ] gitnexus_context on key symbols for callers/callees +- [ ] READ process resource for full execution traces +- [ ] Read source files for implementation details +``` + +## Resources + +| Resource | What you get | +| --------------------------------------- | ------------------------------------------------------- | +| `gitnexus://repo/{name}/context` | Stats, staleness warning (~150 tokens) | +| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores (~300 tokens) | +| `gitnexus://repo/{name}/cluster/{name}` | Area members with file paths (~500 tokens) | +| `gitnexus://repo/{name}/process/{name}` | Step-by-step execution trace (~200 tokens) | + +## Tools + +**gitnexus_query** — find execution flows related to a concept: + +``` +gitnexus_query({query: "payment processing"}) +→ Processes: CheckoutFlow, RefundFlow, WebhookHandler +→ Symbols grouped by flow with file locations +``` + +**gitnexus_context** — 360-degree view of a symbol: + +``` +gitnexus_context({name: "validateUser"}) +→ Incoming calls: loginHandler, apiMiddleware +→ Outgoing calls: checkToken, getUserById +→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3) +``` + +## Example: "How does payment processing work?" + +``` +1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes +2. gitnexus_query({query: "payment processing"}) + → CheckoutFlow: processPayment → validateCard → chargeStripe + → RefundFlow: initiateRefund → calculateRefund → processRefund +3. gitnexus_context({name: "processPayment"}) + → Incoming: checkoutHandler, webhookHandler + → Outgoing: validateCard, chargeStripe, saveTransaction +4. Read src/payments/processor.ts for implementation details +``` diff --git a/.claude/skills/gitnexus/gitnexus-guide/SKILL.md b/.claude/skills/gitnexus/gitnexus-guide/SKILL.md new file mode 100644 index 0000000..937ac73 --- /dev/null +++ b/.claude/skills/gitnexus/gitnexus-guide/SKILL.md @@ -0,0 +1,64 @@ +--- +name: gitnexus-guide +description: "Use when the user asks about GitNexus itself — available tools, how to query the knowledge graph, MCP resources, graph schema, or workflow reference. Examples: \"What GitNexus tools are available?\", \"How do I use GitNexus?\"" +--- + +# GitNexus Guide + +Quick reference for all GitNexus MCP tools, resources, and the knowledge graph schema. + +## Always Start Here + +For any task involving code understanding, debugging, impact analysis, or refactoring: + +1. **Read `gitnexus://repo/{name}/context`** — codebase overview + check index freshness +2. **Match your task to a skill below** and **read that skill file** +3. **Follow the skill's workflow and checklist** + +> If step 1 warns the index is stale, run `npx gitnexus analyze` in the terminal first. + +## Skills + +| Task | Skill to read | +| -------------------------------------------- | ------------------- | +| Understand architecture / "How does X work?" | `gitnexus-exploring` | +| Blast radius / "What breaks if I change X?" | `gitnexus-impact-analysis` | +| Trace bugs / "Why is X failing?" | `gitnexus-debugging` | +| Rename / extract / split / refactor | `gitnexus-refactoring` | +| Tools, resources, schema reference | `gitnexus-guide` (this file) | +| Index, status, clean, wiki CLI commands | `gitnexus-cli` | + +## Tools Reference + +| Tool | What it gives you | +| ---------------- | ------------------------------------------------------------------------ | +| `query` | Process-grouped code intelligence — execution flows related to a concept | +| `context` | 360-degree symbol view — categorized refs, processes it participates in | +| `impact` | Symbol blast radius — what breaks at depth 1/2/3 with confidence | +| `detect_changes` | Git-diff impact — what do your current changes affect | +| `rename` | Multi-file coordinated rename with confidence-tagged edits | +| `cypher` | Raw graph queries (read `gitnexus://repo/{name}/schema` first) | +| `list_repos` | Discover indexed repos | + +## Resources Reference + +Lightweight reads (~100-500 tokens) for navigation: + +| Resource | Content | +| ---------------------------------------------- | ----------------------------------------- | +| `gitnexus://repo/{name}/context` | Stats, staleness check | +| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores | +| `gitnexus://repo/{name}/cluster/{clusterName}` | Area members | +| `gitnexus://repo/{name}/processes` | All execution flows | +| `gitnexus://repo/{name}/process/{processName}` | Step-by-step trace | +| `gitnexus://repo/{name}/schema` | Graph schema for Cypher | + +## Graph Schema + +**Nodes:** File, Function, Class, Interface, Method, Community, Process +**Edges (via CodeRelation.type):** CALLS, IMPORTS, EXTENDS, IMPLEMENTS, DEFINES, MEMBER_OF, STEP_IN_PROCESS + +```cypher +MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"}) +RETURN caller.name, caller.filePath +``` diff --git a/.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md b/.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md new file mode 100644 index 0000000..e19af28 --- /dev/null +++ b/.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md @@ -0,0 +1,97 @@ +--- +name: gitnexus-impact-analysis +description: "Use when the user wants to know what will break if they change something, or needs safety analysis before editing code. Examples: \"Is it safe to change X?\", \"What depends on this?\", \"What will break?\"" +--- + +# Impact Analysis with GitNexus + +## When to Use + +- "Is it safe to change this function?" +- "What will break if I modify X?" +- "Show me the blast radius" +- "Who uses this code?" +- Before making non-trivial code changes +- Before committing — to understand what your changes affect + +## Workflow + +``` +1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this +2. READ gitnexus://repo/{name}/processes → Check affected execution flows +3. gitnexus_detect_changes() → Map current git changes to affected flows +4. Assess risk and report to user +``` + +> If "Index is stale" → run `npx gitnexus analyze` in terminal. + +## Checklist + +``` +- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents +- [ ] Review d=1 items first (these WILL BREAK) +- [ ] Check high-confidence (>0.8) dependencies +- [ ] READ processes to check affected execution flows +- [ ] gitnexus_detect_changes() for pre-commit check +- [ ] Assess risk level and report to user +``` + +## Understanding Output + +| Depth | Risk Level | Meaning | +| ----- | ---------------- | ------------------------ | +| d=1 | **WILL BREAK** | Direct callers/importers | +| d=2 | LIKELY AFFECTED | Indirect dependencies | +| d=3 | MAY NEED TESTING | Transitive effects | + +## Risk Assessment + +| Affected | Risk | +| ------------------------------ | -------- | +| <5 symbols, few processes | LOW | +| 5-15 symbols, 2-5 processes | MEDIUM | +| >15 symbols or many processes | HIGH | +| Critical path (auth, payments) | CRITICAL | + +## Tools + +**gitnexus_impact** — the primary tool for symbol blast radius: + +``` +gitnexus_impact({ + target: "validateUser", + direction: "upstream", + minConfidence: 0.8, + maxDepth: 3 +}) + +→ d=1 (WILL BREAK): + - loginHandler (src/auth/login.ts:42) [CALLS, 100%] + - apiMiddleware (src/api/middleware.ts:15) [CALLS, 100%] + +→ d=2 (LIKELY AFFECTED): + - authRouter (src/routes/auth.ts:22) [CALLS, 95%] +``` + +**gitnexus_detect_changes** — git-diff based impact analysis: + +``` +gitnexus_detect_changes({scope: "staged"}) + +→ Changed: 5 symbols in 3 files +→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline +→ Risk: MEDIUM +``` + +## Example: "What breaks if I change validateUser?" + +``` +1. gitnexus_impact({target: "validateUser", direction: "upstream"}) + → d=1: loginHandler, apiMiddleware (WILL BREAK) + → d=2: authRouter, sessionManager (LIKELY AFFECTED) + +2. READ gitnexus://repo/my-app/processes + → LoginFlow and TokenRefresh touch validateUser + +3. Risk: 2 direct callers, 2 processes = MEDIUM +``` diff --git a/.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md b/.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md new file mode 100644 index 0000000..f48cc01 --- /dev/null +++ b/.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md @@ -0,0 +1,121 @@ +--- +name: gitnexus-refactoring +description: "Use when the user wants to rename, extract, split, move, or restructure code safely. Examples: \"Rename this function\", \"Extract this into a module\", \"Refactor this class\", \"Move this to a separate file\"" +--- + +# Refactoring with GitNexus + +## When to Use + +- "Rename this function safely" +- "Extract this into a module" +- "Split this service" +- "Move this to a new file" +- Any task involving renaming, extracting, splitting, or restructuring code + +## Workflow + +``` +1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents +2. gitnexus_query({query: "X"}) → Find execution flows involving X +3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs +4. Plan update order: interfaces → implementations → callers → tests +``` + +> If "Index is stale" → run `npx gitnexus analyze` in terminal. + +## Checklists + +### Rename Symbol + +``` +- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits +- [ ] Review graph edits (high confidence) and ast_search edits (review carefully) +- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits +- [ ] gitnexus_detect_changes() — verify only expected files changed +- [ ] Run tests for affected processes +``` + +### Extract Module + +``` +- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs +- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers +- [ ] Define new module interface +- [ ] Extract code, update imports +- [ ] gitnexus_detect_changes() — verify affected scope +- [ ] Run tests for affected processes +``` + +### Split Function/Service + +``` +- [ ] gitnexus_context({name: target}) — understand all callees +- [ ] Group callees by responsibility +- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update +- [ ] Create new functions/services +- [ ] Update callers +- [ ] gitnexus_detect_changes() — verify affected scope +- [ ] Run tests for affected processes +``` + +## Tools + +**gitnexus_rename** — automated multi-file rename: + +``` +gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true}) +→ 12 edits across 8 files +→ 10 graph edits (high confidence), 2 ast_search edits (review) +→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}] +``` + +**gitnexus_impact** — map all dependents first: + +``` +gitnexus_impact({target: "validateUser", direction: "upstream"}) +→ d=1: loginHandler, apiMiddleware, testUtils +→ Affected Processes: LoginFlow, TokenRefresh +``` + +**gitnexus_detect_changes** — verify your changes after refactoring: + +``` +gitnexus_detect_changes({scope: "all"}) +→ Changed: 8 files, 12 symbols +→ Affected processes: LoginFlow, TokenRefresh +→ Risk: MEDIUM +``` + +**gitnexus_cypher** — custom reference queries: + +```cypher +MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"}) +RETURN caller.name, caller.filePath ORDER BY caller.filePath +``` + +## Risk Rules + +| Risk Factor | Mitigation | +| ------------------- | ----------------------------------------- | +| Many callers (>5) | Use gitnexus_rename for automated updates | +| Cross-area refs | Use detect_changes after to verify scope | +| String/dynamic refs | gitnexus_query to find them | +| External/public API | Version and deprecate properly | + +## Example: Rename `validateUser` to `authenticateUser` + +``` +1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true}) + → 12 edits: 10 graph (safe), 2 ast_search (review) + → Files: validator.ts, login.ts, middleware.ts, config.json... + +2. Review ast_search edits (config.json: dynamic reference!) + +3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false}) + → Applied 12 edits across 8 files + +4. gitnexus_detect_changes({scope: "all"}) + → Affected: LoginFlow, TokenRefresh + → Risk: MEDIUM — run tests for these flows +``` diff --git a/.gitignore b/.gitignore index bd0acd3..968f891 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ nosetests.xml coverage.xml *,cover .hypothesis/ +/_* # Translations *.mo @@ -61,3 +62,4 @@ target/ #Ipython Notebook .ipynb_checkpoints +.gitnexus diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/RepetierIntegration.iml b/.idea/RepetierIntegration.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/RepetierIntegration.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..947c82b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..f324872 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fe327f9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,43 @@ + +# GitNexus — Code Intelligence + +This project is indexed by GitNexus as **RepetierIntegration** (273 symbols, 575 relationships, 12 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. + +> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. + +## Always Do + +- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. +- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. +- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. +- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`. + +## Never Do + +- NEVER edit a function, class, or method without first running `gitnexus_impact` on it. +- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. +- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph. +- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope. + +## Resources + +| Resource | Use for | +|----------|---------| +| `gitnexus://repo/RepetierIntegration/context` | Codebase overview, check index freshness | +| `gitnexus://repo/RepetierIntegration/clusters` | All functional areas | +| `gitnexus://repo/RepetierIntegration/processes` | All execution flows | +| `gitnexus://repo/RepetierIntegration/process/{name}` | Step-by-step execution trace | + +## CLI + +| Task | Read this skill file | +|------|---------------------| +| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` | +| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` | +| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` | +| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` | +| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` | +| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` | + + \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d8e6704 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,78 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + + +# GitNexus — Code Intelligence + +This project is indexed by GitNexus as **RepetierIntegration** (273 symbols, 575 relationships, 12 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. + +> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. + +## Always Do + +- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. +- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. +- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. +- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`. + +## Never Do + +- NEVER edit a function, class, or method without first running `gitnexus_impact` on it. +- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. +- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph. +- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope. + +## Resources + +| Resource | Use for | +|----------|---------| +| `gitnexus://repo/RepetierIntegration/context` | Codebase overview, check index freshness | +| `gitnexus://repo/RepetierIntegration/clusters` | All functional areas | +| `gitnexus://repo/RepetierIntegration/processes` | All execution flows | +| `gitnexus://repo/RepetierIntegration/process/{name}` | Step-by-step execution trace | + +## CLI + +| Task | Read this skill file | +|------|---------------------| +| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` | +| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` | +| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` | +| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` | +| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` | +| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` | + + + +## Project overview + +RepetierIntegration is a Cura plugin (Python + QML) that adds network printing and print monitoring support for [Repetier-Server](https://www.repetier-server.com/). It is a fork/adaptation of Cura's built-in OctoPrint plugin, reworked to talk to the Repetier-Server HTTP API instead of OctoPrint's. It is installed as a plugin under `[Cura installation]/plugins/RepetierIntegration` — there is no separate build step, package manager, or test suite; it runs inside a live Cura/Uranium application process. + +There is no linter, formatter, or automated test suite configured in this repo. Verify changes by running the plugin inside an actual Cura installation (copy/symlink this folder into Cura's `plugins/RepetierIntegration` directory, or the config folder's `plugins/` dir) and exercising the "Connect Repetier" machine action and print/monitor flow manually. Check the Cura log (Help → Show Configuration Folder → `cura.log`) for `Logger.log` output when debugging — the code logs extensively at the `"d"` (debug) and `"w"` (warning) levels. + +## Architecture + +The plugin registers two Cura extension points from `__init__.py`: + +- **`output_device`** → `RepetierOutputDevicePlugin` (`RepetierOutputDevicePlugin.py`) — discovers Repetier-Server instances (via Zeroconf/Bonjour and manually-added instances stored in Cura preferences under `Repetier/manual_instances`), and creates/destroys `RepetierOutputDevice` instances as printers come and go. It watches `globalContainerStackChanged` to activate the output device that matches the currently selected printer. +- **`machine_action`** → `DiscoverRepetierAction` (`DiscoverRepetierAction.py`, backed by `DiscoverRepetierAction.qml`) — the "Connect Repetier" wizard UI shown in Manage Printers. Handles server discovery list, fetching the printer/slug list and model groups from a Repetier-Server instance, requesting/polling for an API key (or accepting a manually entered one), and validating the key against `printer/api/{slug}?a=getPrinterConfig`. Accepted API keys are cached in Cura preferences (`Repetier/keys_cache`, base64-encoded JSON) so they don't need re-entry per machine. + +Core runtime class: + +- **`RepetierOutputDevice`** (`RepetierOutputDevice.py`) extends Cura's `NetworkedPrinterOutputDevice`. This is where the actual printer connection lives: polling printer/job status from the Repetier-Server REST API, uploading G-code (as a `QHttpMultiPart` request) and triggering print start, and driving Cura's print-monitor UI (`qml/MonitorItem.qml`, `RepetierComponents.qml`) via `PrinterOutputModel`/`PrintJobOutputModel`. `UnifiedConnectionState` exists to paper over a `ConnectionState` enum casing change between Cura 4.0 beta1 and beta2 — keep both branches if touching connection-state handling. + +Supporting modules: + +- **`NetworkMJPGImage.py`** — a `QQuickPaintedItem` registered as a QML type (`RepetierIntegration.NetworkMJPGImage`) that streams and paints an MJPEG webcam feed, including flip/rotate handling driven by the webcam orientation settings fetched during discovery. +- **`NetworkReplyTimeout.py`** — small `QTimer` wrapper that aborts a `QNetworkReply` if it takes too long, used to bound blocking network calls in the discovery/API-key flow. + +QML files (`RepetierComponents.qml`, `DiscoverRepetierAction.qml`, `qml/MonitorItem.qml`) are the UI counterparts driven by `pyqtProperty`/`pyqtSlot`/`pyqtSignal` members on `DiscoverRepetierAction` and `RepetierOutputDevice` — when changing a Python-side property/signal name, grep the corresponding `.qml` file for its usage since there's no compiler to catch a mismatch. + +## Key conventions and gotchas + +- **Preferences namespace**: all Cura preference keys use the `Repetier/` prefix (`Repetier/manual_instances`, `Repetier/keys_cache`). Per-machine metadata keys use the `repetier_` prefix (`repetier_id`, `repetier_api_key`, `repetier_show_camera`). +- **Zeroconf `properties` dict**: instance properties passed around (`addInstance`, `addManualInstance`) use `bytes` keys and values (e.g. `b"path"`, `b"repetier_id"`) to mirror what the real `zeroconf` library hands back for discovered (non-manual) instances — preserve this shape when adding new properties. +- **SDK version compatibility**: `plugin.json` declares `supported_sdk_versions` for Cura 5.0 through 8.0 (`minimum_cura_version: 5.0`). `RepetierOutputDevice.py`'s `UnifiedConnectionState` class is an example of the defensive `try`/`except AttributeError` pattern used to support multiple Cura/Uranium API shapes — follow the same pattern rather than assuming a single SDK version's API surface. +- **Renaming a printer breaks the plugin** (documented in README): the plugin's connection state matching relies on the machine's Cura ID, and Cura has a known bug where renaming loses that association. \ No newline at end of file diff --git a/Cura 4.1 testing doc.rtf.zip b/Cura 4.1 testing doc.rtf.zip deleted file mode 100644 index ce7dd34..0000000 Binary files a/Cura 4.1 testing doc.rtf.zip and /dev/null differ diff --git a/DiscoverRepetierAction.py b/DiscoverRepetierAction.py index 7978d1c..190942d 100644 --- a/DiscoverRepetierAction.py +++ b/DiscoverRepetierAction.py @@ -15,8 +15,7 @@ from PyQt6.QtQml import QQmlComponent, QQmlContext from PyQt6.QtGui import QDesktopServices from PyQt6.QtWidgets import QMessageBox -from PyQt6.QtNetwork import QNetworkRequest, QNetworkAccessManager, QNetworkReply -from .NetworkReplyTimeout import NetworkReplyTimeout +from PyQt6.QtNetwork import QNetworkRequest, QNetworkAccessManager, QNetworkReply, QSslConfiguration, QSslSocket from .RepetierOutputDevicePlugin import RepetierOutputDevicePlugin from .RepetierOutputDevice import RepetierOutputDevice @@ -50,11 +49,11 @@ def __init__(self, parent: QObject = None) -> None: self._network_manager = QNetworkAccessManager() self._network_manager.finished.connect(self._onRequestFinished) self._printers = [""] + self._is_fetching_printers = False self._groups = [""] self._printerlist_reply = None self._groupslist_reply = None self._settings_reply = None - self._settings_reply_timeout = None # type: Optional[NetworkReplyTimeout] self._instance_supports_appkeys = False self._appkey_reply = None # type: Optional[QNetworkReply] @@ -196,6 +195,15 @@ def instanceId(self) -> str: return global_container_stack.getMetaDataEntry("repetier_id", "") + ## The discovery/manual instance id (RepetierOutputDevice.getId()) linked to the active machine. + @pyqtProperty(str, notify = instanceIdChanged) + def linkedInstanceId(self) -> str: + global_container_stack = self._application.getGlobalContainerStack() + if not global_container_stack: + return "" + + return global_container_stack.getMetaDataEntry("repetier_instance_id", "") + @pyqtSlot(str) @pyqtSlot(result = str) def getInstanceId(self) -> str: @@ -257,12 +265,14 @@ def probeAppKeySupport(self, instance_id: str) -> None: self._appkey_reply = self._network_manager.get(appkey_probe_request) @pyqtSlot(str) - def getPrinterList(self, base_url): + def getPrinterList(self, base_url): self._instance_responded = False + self._is_fetching_printers = True + self.isFetchingPrintersChanged.emit() Logger.log("d", "getPrinterList:base_url:" + base_url) url = QUrl( base_url + "printer/info") Logger.log("d", "getPrinterList:" + url.toString()) - settings_request = QNetworkRequest(url) + settings_request = QNetworkRequest(url) settings_request.setRawHeader("User-Agent".encode(), self._user_agent) self._printerlist_reply=self._network_manager.get(settings_request) return self._printers @@ -293,8 +303,6 @@ def testApiKey(self,instance_id: str, base_url, api_key, basic_auth_username = " if self._settings_reply.isRunning(): self._settings_reply.abort() self._settings_reply = None - if self._settings_reply_timeout: - self._settings_reply_timeout = None if ((api_key != "") and (api_key != None) and (work_id != "")): Logger.log("d", "Trying to access Repetier instance at %s with the provided API key." % base_url) Logger.log("d", "Using %s as work_id" % work_id) @@ -341,11 +349,17 @@ def getApiKey(self, instance_id: str) -> str: return api_key selectedInstanceSettingsChanged = pyqtSignal() + printersChanged = pyqtSignal() + isFetchingPrintersChanged = pyqtSignal() - @pyqtProperty(list) + @pyqtProperty(list, notify = printersChanged) def getPrinters(self): return self._printers + @pyqtProperty(bool, notify = isFetchingPrintersChanged) + def isFetchingPrinters(self): + return self._is_fetching_printers + @pyqtProperty(list) def getGroups(self): return self._groups @@ -478,6 +492,10 @@ def _onRequestFailed(self, reply: QNetworkReply) -> None: # Handler for all requests that have finished. def _onRequestFinished(self, reply: QNetworkReply) -> None: + if "printer/info" in reply.url().toString() and self._is_fetching_printers: + self._is_fetching_printers = False + self.isFetchingPrintersChanged.emit() + if reply.error() == QNetworkReplyNetworkErrors.TimeoutError: # if reply.error() == QNetworkReply.TimeoutError: QMessageBox.warning(None,'Connection Timeout','Connection Timeout') @@ -510,6 +528,7 @@ def _onRequestFinished(self, reply: QNetworkReply) -> None: for printerinfo in json_data["printers"]: Logger.log("d", "Slug: %s",printerinfo["slug"]) self._printers.append(printerinfo["slug"]) + self.printersChanged.emit() if "apikey" in json_data: Logger.log("d", "DiscoverRepetierAction: apikey: %s",json_data["apikey"]) @@ -593,7 +612,7 @@ def _onRequestFinished(self, reply: QNetworkReply) -> None: def _createRequest(self, url: str, basic_auth_username: str = "", basic_auth_password: str = "") -> QNetworkRequest: request = QNetworkRequest(url) - request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) + request.setAttribute(QNetworkRequest.Attribute.FollowRedirectsAttribute, True) request.setRawHeader(b"User-Agent", self._user_agent) if basic_auth_username and basic_auth_password: @@ -602,7 +621,7 @@ def _createRequest(self, url: str, basic_auth_username: str = "", basic_auth_pas # ignore SSL errors (eg for self-signed certificates) ssl_configuration = QSslConfiguration.defaultConfiguration() - ssl_configuration.setPeerVerifyMode(QSslSocket.VerifyNone) + ssl_configuration.setPeerVerifyMode(QSslSocket.PeerVerifyMode.VerifyNone) request.setSslConfiguration(ssl_configuration) return request diff --git a/DiscoverRepetierAction.qml b/DiscoverRepetierAction.qml index 8a344f9..84485c8 100644 --- a/DiscoverRepetierAction.qml +++ b/DiscoverRepetierAction.qml @@ -108,28 +108,14 @@ Cura.MachineAction enabled: base.selectedInstance != null && base.selectedInstance.getProperty("manual") == "true" onClicked: { - if (Cura.ContainerManager.getContainerMetaDataEntry(base.selectedInstance.name, "repetier_id") != null) - { - manualPrinterDialog.showDialog( - base.selectedInstance.name, base.selectedInstance.ipAddress, - base.selectedInstance.port, base.selectedInstance.path, - base.selectedInstance.getProperty("useHttps") == "true", - base.selectedInstance.getProperty("userName"), - base.selectedInstance.getProperty("password"), - Cura.ContainerManager.getContainerMetaDataEntry(base.selectedInstance.name, "repetier_id") - ); - } - else - { - manualPrinterDialog.showDialog( - base.selectedInstance.name, base.selectedInstance.ipAddress, - base.selectedInstance.port, base.selectedInstance.path, - base.selectedInstance.getProperty("useHttps") == "true", - base.selectedInstance.getProperty("userName"), - base.selectedInstance.getProperty("password"), - "" - ); - } + manualPrinterDialog.showDialog( + base.selectedInstance.name, base.selectedInstance.ipAddress, + base.selectedInstance.port, base.selectedInstance.path, + base.selectedInstance.getProperty("useHttps") == "true", + base.selectedInstance.getProperty("userName"), + base.selectedInstance.getProperty("password"), + base.selectedInstance.repetier_id + ); } } @@ -176,7 +162,7 @@ Cura.MachineAction model: manager.discoveredInstances onModelChanged: { - var selectedId = manager.instanceId; + var selectedId = manager.linkedInstanceId; for(var i = 0; i < model.length; i++) { if(model[i].getId() == selectedId) { @@ -209,7 +195,7 @@ Cura.MachineAction anchors.right: parent.right text: listview.model[index].name elide: Text.ElideRight - font.italic: listview.model[index].key == manager.instanceId + font.weight: listview.model[index].getId() == manager.linkedInstanceId ? 625 : Font.Normal wrapMode: Text.NoWrap } @@ -281,8 +267,8 @@ Cura.MachineAction UM.Label { id: lblRepID - width: Math.floor(parent.width * 0.2) - text: base.selectedInstance ? Cura.ContainerManager.getContainerMetaDataEntry(base.selectedInstance.name, "repetier_id") : "" + width: Math.floor(parent.width * 0.2) + text: base.selectedInstance ? base.selectedInstance.repetier_id : "" } UM.Label { @@ -293,8 +279,8 @@ Cura.MachineAction Cura.TextField { id: apiKey - width: Math.floor(parent.width * 0.8 - UM.Theme.getSize("default_margin").width) - text: base.selectedInstance ? Cura.ContainerManager.getContainerMetaDataEntry(base.selectedInstance.name, "repetier_api_key") : "" + width: Math.floor(parent.width * 0.8 - UM.Theme.getSize("default_margin").width) + text: Cura.ContainerManager.getContainerMetaDataEntry(Cura.MachineManager.activeMachine.id, "repetier_api_key") onTextChanged: { apiCheckDelay.throttledCheck() @@ -308,9 +294,8 @@ Cura.MachineAction { if(base.selectedInstance != null) { - lblRepID.text = Cura.ContainerManager.getContainerMetaDataEntry(base.selectedInstance.name, "repetier_id") - apiKey.text = Cura.ContainerManager.getContainerMetaDataEntry(base.selectedInstance.name, "repetier_api_key") - //apiKey.text = manager.getApiKey(base.selectedInstance.getId()) + lblRepID.text = base.selectedInstance.repetier_id + apiKey.text = Cura.ContainerManager.getContainerMetaDataEntry(Cura.MachineManager.activeMachine.id, "repetier_api_key") } } } @@ -450,7 +435,8 @@ Cura.MachineAction { if(manager.getGroups[i] != "") { - comboGroups.append({ label: catalog.i18nc("@action:ComboBox option", manager.getGroups[i]), key: manager.getGroups[i] }); + var groupLabel = manager.getGroups[i] == "#" ? catalog.i18nc("@action:ComboBox option", "(Default)") : catalog.i18nc("@action:ComboBox option", manager.getGroups[i]); + comboGroups.append({ label: groupLabel, key: manager.getGroups[i] }); } } } @@ -466,8 +452,12 @@ Cura.MachineAction } } } + if (current_index == -1 && comboGroups.count > 0) + { + current_index = 0; + } comboGroupsctl.currentIndex = current_index; - comboGroupsctl.populatingModel = false; + comboGroupsctl.populatingModel = false; } } currentIndex: @@ -622,6 +612,12 @@ Cura.MachineAction } } + UM.Label + { + visible: base.selectedInstance != null && base.selectedInstance.getId() == manager.linkedInstanceId + text: catalog.i18nc("@label", "This is the Repetier instance currently linked to this printer.") + } + Flow { visible: base.selectedInstance != null @@ -636,18 +632,19 @@ Cura.MachineAction Cura.SecondaryButton { text: catalog.i18nc("@action:button", "Connect") - enabled: apiKey.text != "" && manager.instanceApiKeyAccepted + enabled: apiKey.text != "" && manager.instanceApiKeyAccepted && base.selectedInstance.getId() != manager.linkedInstanceId onClicked: { if(fixGcodeFlavor.visible) { manager.applyGcodeFlavorFix(fixGcodeFlavor.checked) - } + } + manager.setContainerMetaDataEntry(Cura.MachineManager.activeMachine.id, "repetier_instance_id", base.selectedInstance.getId()) manager.setInstanceId(base.selectedInstance.repetier_id) manager.setApiKey(apiKey.text) completed() } - } + } } } } @@ -716,7 +713,6 @@ Cura.MachineAction pathText = "/" + pathText // ensure absolute path } manager.setManualInstance(nameText, addressText, parseInt(portText), pathText, httpsCheckbox.checked, userNameText, passwordText, repidText) - manager.setContainerMetaDataEntry(Cura.MachineManager.activeMachine.id, "repetier_id", repidText) } Column { @@ -783,16 +779,35 @@ Cura.MachineAction Cura.SecondaryButton { text: catalog.i18nc("@action:button","Get Printers") + enabled: !manager.isFetchingPrinters onClicked: { manager.getPrinterList("http://" + manualPrinterDialog.addressText.trim()+":"+manualPrinterDialog.portText.trim()+"/") - if (manager.getPrinters.length>0) + } + } + Connections + { + target: manager + function onPrintersChanged() + { + var previousKey = (repid.currentIndex >= 0 && comboPrinters.get(repid.currentIndex) !== undefined) ? comboPrinters.get(repid.currentIndex).key : undefined + + comboPrinters.clear() + for (var i = 0; i < manager.getPrinters.length; i++) + if(manager.getPrinters[i] != "") + comboPrinters.append({ label: catalog.i18nc("@action:ComboBox option", manager.getPrinters[i]), key: manager.getPrinters[i] }) + + if (previousKey !== undefined) + { + for (var j = 0; j < comboPrinters.count; j++) { - comboPrinters.clear() - for (var i =0;i None: - super().__init__() - - self._reply = reply - self._callback = callback - - self._timer = QTimer() - self._timer.setInterval(timeout) - self._timer.setSingleShot(True) - self._timer.timeout.connect(self._onTimeout) - - self._timer.start() - - def _onTimeout(self): - if self._reply.isRunning(): - self._reply.abort() - if self._callback: - self._callback(self._reply) - self.timeout.emit(self._reply) diff --git a/README.md b/README.md index fd55f37..d93c92d 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,39 @@ -# RepetierIntegration -# Version 5.1 -Shane Bumpurs - -Cura plugin which enables printing directly to Repetier and monitoring the progress -The name has changed to RepetierIntegration in the plugin folder. -This plugin is basically a copy of the Octoprint plugin with the necessary changes to work with repetier server. -Updated to 5.1, this version will not work with 5.0+. - -Installation ----- -* Manually: - - Make sure your Cura version is 5.0+ - - Download or clone the repository into [Cura installation folder]/plugins/RepetierIntegration - or in the plugins folder inside the configuration folder. The configuration folder can be - found via Help -> Show Configuration Folder inside Cura. - NB: The folder of the plugin itself *must* be ```RepetierIntegration``` - NB: Make sure you download the branch that matches the Cura branch (ie: 3.1 for Cura 2.2-3.1, 3.2 for Cura 3.2, 3.3 for Cura 3.3 etc) - -Blurry Youtube Video showing Install -https://youtu.be/VHw93Pt_QIo - -How to use ----- -- Make sure Repetier is up and running -- In Cura, under Manage printers select your printer. -- Select "Connect to Repetier" on the Manage Printers page. -- Click add and **make sure you match the Name you give it in the plugin, with the name of the Printer in Cura.** -- Fill in the IP and Port, if you have security turned on, click the advanced checkbox and enter that information -- Click Get Printers button, it should populate the dropdown to select your repetier printer. -- Click OK this will show the printer in the Printers list again but then ask for your Repetier API key. Once that is filled you can check the extra options if you have a webcam and need to rotate it. -- If you do not want to print immediately but have your print job stored uncheck "Automatically start print job after uploading" -- From this point on, the print monitor should be functional and you should be able to switch to "Print to Repetier" on the bottom of the sidebar. - - Config example: - ![alt text](https://user-images.githubusercontent.com/12956626/59142707-9d0d5e00-8987-11e9-94f7-53bc2707e3d1.jpg "Config") - - Cura has a bug so that if you have ever renamed your printer this plugin won't work. You'll have to create a new printer from scratch. - +# RepetierIntegration +# Version 5.2 +Mick Mifsud + +### 🛠️ Bugfixed by Claude — connection setup now works as expected + +The connect-a-new-printer flow used to be flaky (API key not sticking, printer selection resetting, +repeated delete/re-add just to get a connection). Those issues have been found and fixed; see the +git history for details. Ordinary use once connected was never affected. + +Cura plugin which enables printing directly to Repetier and monitoring the progress +The name has changed to RepetierIntegration in the plugin folder. +This plugin is basically a copy of the Octoprint plugin with the necessary changes to work with repetier server. +Updated to 5.1, this version will not work with 5.0+. + +Installation +---- +* Manually: + - Make sure your Cura version is 5.0+ + - Download or clone the repository into [Cura installation folder]/plugins/RepetierIntegration + or in the plugins folder inside the configuration folder. The configuration folder can be + found via Help -> Show Configuration Folder inside Cura. + NB: The folder of the plugin itself *must* be ```RepetierIntegration``` + NB: Make sure you download the branch that matches the Cura branch (ie: 3.1 for Cura 2.2-3.1, 3.2 for Cura 3.2, 3.3 for Cura 3.3 etc) + +Blurry Youtube Video showing Install +https://youtu.be/VHw93Pt_QIo + +How to use +---- +- Make sure Repetier is up and running +- In Cura, under Manage printers select your printer. +- Select "Connect to Repetier" on the Manage Printers page. +- Click add, fill in the IP and Port, if you have security turned on, click the advanced checkbox and enter that information +- Click Get Printers button, it should populate the dropdown to select your repetier printer. +- Click OK this will show the printer in the Printers list again but then ask for your Repetier API key. Once that is filled you can check the extra options if you have a webcam and need to rotate it. +- Once the API key is accepted, the "Connect" button becomes available — click it to link this Repetier instance to the current Cura printer (the linked instance is shown in bold in the list). +- If you do not want to print immediately but have your print job stored uncheck "Automatically start print job after uploading" +- From this point on, the print monitor should be functional and you should be able to switch to "Print to Repetier" on the bottom of the sidebar. diff --git a/RepetierOutputDevice.py b/RepetierOutputDevice.py index b479ae5..ec12dfc 100644 --- a/RepetierOutputDevice.py +++ b/RepetierOutputDevice.py @@ -318,6 +318,10 @@ def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None ## Start requesting data from the instance def connect(self) -> None: + if self._connection_state in (UnifiedConnectionState.Connecting, UnifiedConnectionState.Connected): + # Already (re)connecting; the running poll timer picks up a changed API key on its own. + return + self._createNetworkManager() self.setConnectionState(cast(ConnectionState, UnifiedConnectionState.Connecting)) @@ -489,7 +493,7 @@ def _startPrint(self) -> None: post_parts.append(post_part) destination = "local" - if self._sd_supported and parseBool(global_container_stack.getMetaDataEntry("Repetier_store_sd", False)): + if self._sd_supported and parseBool(global_container_stack.getMetaDataEntry("repetier_store_sd", False)): destination = "sdcard" try: @@ -608,12 +612,13 @@ def _onRequestFinished(self, reply: QNetworkReply) -> None: if not self._printers: self._createPrinterList() printer = self._printers[0] + printer_state = "idle" if http_status_code == 200: if not self.acceptsCommands: self._setAcceptsCommands(True) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to Repetier on {0}").format(self._repetier_id)) - if self._connection_state == UnifiedConnectionState.Connecting: + if self._connection_state != UnifiedConnectionState.Connected: self.setConnectionState(cast(ConnectionState, UnifiedConnectionState.Connected)) try: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) @@ -672,15 +677,17 @@ def _onRequestFinished(self, reply: QNetworkReply) -> None: printer.updateTargetBedTemperature(0) printer.updateState(printer_state) except: - Logger.log("w", "Received invalid JSON from Repetier instance.2") + Logger.log("w", "Received invalid JSON from Repetier instance.2") json_data = {} - printer.activePrintJob.updateState("offline") + if printer.activePrintJob: + printer.activePrintJob.updateState("offline") self.setConnectionText(i18n_catalog.i18nc("@info:status", "Repetier on {0} configuration is invalid").format(self._repetier_id)) elif http_status_code == 401: printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") + self.setConnectionState(cast(ConnectionState, UnifiedConnectionState.Error)) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Repetier on {0} does not allow access to print").format(self._repetier_id)) error_handled = True elif http_status_code == 409: @@ -753,7 +760,7 @@ def _onRequestFinished(self, reply: QNetworkReply) -> None: except: if printer.activePrintJob is not None: printer.activePrintJob.updateState("offline") - self.setConnectionText(i18n_catalog.i18nc("@info:status", "Repetier on {0} configuration is invalid").format(self._key)) + self.setConnectionText(i18n_catalog.i18nc("@info:status", "Repetier on {0} configuration is invalid").format(self._repetier_id)) else: if printer.activePrintJob is not None: printer.activePrintJob.updateState("offline") @@ -971,6 +978,11 @@ def _onUploadFinished(self, reply: QNetworkReply) -> None: message.actionTriggered.connect(self._openRepetierPrint) message.show() elif self._auto_print: + if not location_url: + # Repetier-Server starts the print itself as part of the job upload and does not + # return a Location header (unlike the OctoPrint API this code was ported from). + Logger.log("d", "No Location header on job upload; Repetier already started the print itself.") + return end_point = location_url.toString().split(self._api_prefix, 1)[1] if self._ufp_supported and end_point.endswith(".ufp"): end_point += ".gcode" diff --git a/RepetierOutputDevicePlugin.py b/RepetierOutputDevicePlugin.py index 4271bca..813ec40 100644 --- a/RepetierOutputDevicePlugin.py +++ b/RepetierOutputDevicePlugin.py @@ -130,17 +130,39 @@ def getInstanceById(self, instance_id: str) -> Optional[RepetierOutputDevice]: Logger.log("w", "No instance found with id %s", instance_id) return None + ## Whether instance `key` is the one linked to the active machine via "repetier_instance_id". + # Falls back to the legacy (coincidental) match on Cura's own container "id" for older + # profiles, self-healing by backfilling "repetier_instance_id" when that happens. + def _isLinkedInstance(self, key: str, global_container_stack) -> bool: + linked_id = global_container_stack.getMetaDataEntry("repetier_instance_id", "") + if linked_id: + return key == linked_id + + if key == global_container_stack.getMetaDataEntry("id"): + global_container_stack.setMetaDataEntry("repetier_instance_id", key) + return True + + return False + + ## (Re)connect connectionStateChanged without stacking duplicate connections on repeat calls. + def _connectInstanceSignals(self, instance: RepetierOutputDevice) -> None: + try: + instance.connectionStateChanged.disconnect(self._onInstanceConnectionStateChanged) + except TypeError: + pass + instance.connectionStateChanged.connect(self._onInstanceConnectionStateChanged) + def reCheckConnections(self) -> None: global_container_stack = Application.getInstance().getGlobalContainerStack() if not global_container_stack: return for key in self._instances: - if key == global_container_stack.getMetaDataEntry("id"): + if self._isLinkedInstance(key, global_container_stack): api_key = global_container_stack.getMetaDataEntry("repetier_api_key", "") self._instances[key].setApiKey(api_key) self._instances[key].setShowCamera(parseBool(global_container_stack.getMetaDataEntry("repetier_show_camera", "true"))) - self._instances[key].connectionStateChanged.connect(self._onInstanceConnectionStateChanged) + self._connectInstanceSignals(self._instances[key]) self._instances[key].connect() else: if self._instances[key].isConnected(): @@ -151,11 +173,11 @@ def addInstance(self, name: str, address: str, port: int, properties: Dict[bytes instance = RepetierOutputDevice(name, address, port, properties) self._instances[instance.getId()] = instance global_container_stack = Application.getInstance().getGlobalContainerStack() - if global_container_stack and instance.getId() == global_container_stack.getMetaDataEntry("id"): + if global_container_stack and self._isLinkedInstance(instance.getId(), global_container_stack): api_key = global_container_stack.getMetaDataEntry("repetier_api_key", "") instance.setApiKey(api_key) instance.setShowCamera(parseBool(global_container_stack.getMetaDataEntry("repetier_show_camera", "true"))) - instance.connectionStateChanged.connect(self._onInstanceConnectionStateChanged) + self._connectInstanceSignals(instance) instance.connect() def removeInstance(self, name: str) -> None: diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..18a5f66 --- /dev/null +++ b/changelog.md @@ -0,0 +1,75 @@ +# Changelog + +### v5.2 - connection setup bugfixes (Mick Mifsud) + +- **Flaky first-time connection setup.** `RepetierOutputDevicePlugin` decided whether to push + the API key into a live connection by comparing the Repetier server instance's own name + against Cura's auto-generated machine container `id` — two unrelated values that only matched + by coincidence. This is why setup required repeatedly deleting/re-adding the printer profile. + Fixed by persisting an explicit `repetier_instance_id` link on the machine when "Connect" is + clicked, and matching on that. Existing (accidentally-working) profiles are migrated + automatically on first reconnect (legacy id-match still honored as a fallback, then + self-heals by backfilling the new metadata). +- **API key never validating on new profiles**, leaving "Connect" permanently disabled. The + wizard's "RepetierID" and "API Key" fields were reading Cura container metadata keyed by the + *Repetier server instance's name* instead of the *active machine's id* — essentially never a + valid lookup for a fresh profile. Fixed to read the printer slug directly off the selected + instance and the API key from the active machine. +- Same root-cause bug (server-instance identity vs. machine id) was also present in the + instance-list "linked instance" bolding (`font.italic` was comparing against a nonexistent + `.key` property and the wrong metadata field) and in the "Edit" button's pre-fill logic for + the manual-instance dialog. Both fixed to use the same reliable source. +- **"Get Printers" button required multiple clicks**, and appeared to "cache" against the IP. + `getPrinterList()` fires an async network request but the QML read the result synchronously + right after calling it — always one click behind. Added a `printersChanged` signal (properly + notified when the response actually lands) and an `isFetchingPrinters` busy flag; the button + now disables while in flight and the dropdown populates reliably on the first response. + Re-fetching now also preserves the current selection if it's still present in the new list. +- **`UnboundLocalError` crash silently breaking printer status polling** whenever a `stateList` + response omitted `numExtruder` (seen with a real printer profile) — `printer_state` was only + assigned inside a conditional branch but used unconditionally afterward. The `except:` handler + for this case then also crashed on `printer.activePrintJob` being `None`. Both fixed. +- A second, identical `AttributeError` landmine in the `listPrinter` response handler + (`self._key` was never defined anywhere — should have been `self._repetier_id`). +- **"Store G-code on the printer SD card" checkbox silently did nothing** — the setting was + read back with a case-mismatched metadata key (`Repetier_store_sd` vs. the stored + `repetier_store_sd`), so it always evaluated to the default (off). + Fixed to use the matching, correctly-cased key. +- Unguarded `location_url` dereference in the upload-finished handler — would crash if Repetier + ever omits the `Location` header on a successful auto-print upload. Added a guard with a + clear error message instead. +- Missing `QSslConfiguration`/`QSslSocket` imports and PyQt6 enum-scoping issues + (`FollowRedirectsAttribute`, `VerifyNone`) in `DiscoverRepetierAction._createRequest()` — + currently unreachable dead code (not wired to any UI button) but would have crashed + immediately if ever invoked. +- Duplicate `connectionStateChanged` signal connections stacking up on every reconnect attempt, + causing repeated/duplicated `addOutputDevice`/`removeOutputDevice` calls during setup. Now + disconnects before reconnecting. +- `RepetierOutputDevice.connect()` had no idempotency guard, so rapid successive calls (e.g. + from `setInstanceId()` and `setApiKey()` each triggering a reconnect) restarted the poll timer + and re-issued a burst of duplicate HTTP requests. Now a no-op while already + connecting/connected. +- Invalid API key (HTTP 401) left the connection silently stuck in "Connecting" with no visible + error state. Now surfaces a proper `Error` state and recovers to `Connected` automatically on + the next successful poll once the key is corrected. + +### Changed + +- Instance list now bolds (moderate weight, not full bold) the Repetier instance currently + linked to the active printer, and shows a "currently linked" label when it's selected. +- "Connect" is disabled while viewing the already-linked instance and enables as soon as a + different instance is selected. +- Group dropdown ("Store print job and print") now renders Repetier-Server's `#` (default/ + ungrouped bucket) as "(Default)" instead of the raw symbol, and defaults to the first + available group instead of leaving no selection. +- `manualPrinterDialog.onAccepted` no longer writes `repetier_id` metadata onto whichever + machine happens to be active just from adding/editing a server entry in the list — that link + is only ever made via the explicit "Connect" action now. + +### Removed + +- Dead code: `NetworkReplyTimeout.py` (never instantiated) and all references to it. +- `config.jpg`, `webcam.jpg`, and `Cura 4.1 testing doc.rtf.zip` — stale screenshots/testing doc + from the Cura 4.1 era, superseded by the current README instructions. +- README's "must match the Instance Name" workaround note — no longer applicable now that + linking is explicit rather than name-coincidence-based. diff --git a/config.jpg b/config.jpg deleted file mode 100644 index b725134..0000000 Binary files a/config.jpg and /dev/null differ diff --git a/webcam.jpg b/webcam.jpg deleted file mode 100644 index 2cee4d7..0000000 Binary files a/webcam.jpg and /dev/null differ