diff --git a/graphify/skills/agents/references/add-watch.md b/graphify/skills/agents/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/agents/references/add-watch.md +++ b/graphify/skills/agents/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/agents/references/exports.md b/graphify/skills/agents/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/agents/references/exports.md +++ b/graphify/skills/agents/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/agents/references/query.md b/graphify/skills/agents/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/agents/references/query.md +++ b/graphify/skills/agents/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/agents/references/transcribe.md b/graphify/skills/agents/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/agents/references/transcribe.md +++ b/graphify/skills/agents/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/agents/references/update.md b/graphify/skills/agents/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/agents/references/update.md +++ b/graphify/skills/agents/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/graphify/skills/amp/references/add-watch.md b/graphify/skills/amp/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/amp/references/add-watch.md +++ b/graphify/skills/amp/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/amp/references/exports.md b/graphify/skills/amp/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/amp/references/exports.md +++ b/graphify/skills/amp/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/amp/references/query.md b/graphify/skills/amp/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/amp/references/query.md +++ b/graphify/skills/amp/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/amp/references/transcribe.md b/graphify/skills/amp/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/amp/references/transcribe.md +++ b/graphify/skills/amp/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/amp/references/update.md b/graphify/skills/amp/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/amp/references/update.md +++ b/graphify/skills/amp/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/graphify/skills/claude/references/add-watch.md b/graphify/skills/claude/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/claude/references/add-watch.md +++ b/graphify/skills/claude/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/claude/references/exports.md b/graphify/skills/claude/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/claude/references/exports.md +++ b/graphify/skills/claude/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/claude/references/query.md b/graphify/skills/claude/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/claude/references/query.md +++ b/graphify/skills/claude/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/claude/references/transcribe.md b/graphify/skills/claude/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/claude/references/transcribe.md +++ b/graphify/skills/claude/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/claude/references/update.md b/graphify/skills/claude/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/claude/references/update.md +++ b/graphify/skills/claude/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/graphify/skills/claw/references/add-watch.md b/graphify/skills/claw/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/claw/references/add-watch.md +++ b/graphify/skills/claw/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/claw/references/exports.md b/graphify/skills/claw/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/claw/references/exports.md +++ b/graphify/skills/claw/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/claw/references/query.md b/graphify/skills/claw/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/claw/references/query.md +++ b/graphify/skills/claw/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/claw/references/transcribe.md b/graphify/skills/claw/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/claw/references/transcribe.md +++ b/graphify/skills/claw/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/claw/references/update.md b/graphify/skills/claw/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/claw/references/update.md +++ b/graphify/skills/claw/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/graphify/skills/codex/references/add-watch.md b/graphify/skills/codex/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/codex/references/add-watch.md +++ b/graphify/skills/codex/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/codex/references/exports.md b/graphify/skills/codex/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/codex/references/exports.md +++ b/graphify/skills/codex/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/codex/references/query.md b/graphify/skills/codex/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/codex/references/query.md +++ b/graphify/skills/codex/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/codex/references/transcribe.md b/graphify/skills/codex/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/codex/references/transcribe.md +++ b/graphify/skills/codex/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/codex/references/update.md b/graphify/skills/codex/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/codex/references/update.md +++ b/graphify/skills/codex/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/graphify/skills/copilot/references/add-watch.md b/graphify/skills/copilot/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/copilot/references/add-watch.md +++ b/graphify/skills/copilot/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/copilot/references/exports.md b/graphify/skills/copilot/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/copilot/references/exports.md +++ b/graphify/skills/copilot/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/copilot/references/query.md b/graphify/skills/copilot/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/copilot/references/query.md +++ b/graphify/skills/copilot/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/copilot/references/transcribe.md b/graphify/skills/copilot/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/copilot/references/transcribe.md +++ b/graphify/skills/copilot/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/copilot/references/update.md b/graphify/skills/copilot/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/copilot/references/update.md +++ b/graphify/skills/copilot/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/graphify/skills/droid/references/add-watch.md b/graphify/skills/droid/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/droid/references/add-watch.md +++ b/graphify/skills/droid/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/droid/references/exports.md b/graphify/skills/droid/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/droid/references/exports.md +++ b/graphify/skills/droid/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/droid/references/query.md b/graphify/skills/droid/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/droid/references/query.md +++ b/graphify/skills/droid/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/droid/references/transcribe.md b/graphify/skills/droid/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/droid/references/transcribe.md +++ b/graphify/skills/droid/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/droid/references/update.md b/graphify/skills/droid/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/droid/references/update.md +++ b/graphify/skills/droid/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/graphify/skills/kilo/references/add-watch.md b/graphify/skills/kilo/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/kilo/references/add-watch.md +++ b/graphify/skills/kilo/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/kilo/references/exports.md b/graphify/skills/kilo/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/kilo/references/exports.md +++ b/graphify/skills/kilo/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/kilo/references/query.md b/graphify/skills/kilo/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/kilo/references/query.md +++ b/graphify/skills/kilo/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/kilo/references/transcribe.md b/graphify/skills/kilo/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/kilo/references/transcribe.md +++ b/graphify/skills/kilo/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/kilo/references/update.md b/graphify/skills/kilo/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/kilo/references/update.md +++ b/graphify/skills/kilo/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/graphify/skills/kiro/references/add-watch.md b/graphify/skills/kiro/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/kiro/references/add-watch.md +++ b/graphify/skills/kiro/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/kiro/references/exports.md b/graphify/skills/kiro/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/kiro/references/exports.md +++ b/graphify/skills/kiro/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/kiro/references/query.md b/graphify/skills/kiro/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/kiro/references/query.md +++ b/graphify/skills/kiro/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/kiro/references/transcribe.md b/graphify/skills/kiro/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/kiro/references/transcribe.md +++ b/graphify/skills/kiro/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/kiro/references/update.md b/graphify/skills/kiro/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/kiro/references/update.md +++ b/graphify/skills/kiro/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/graphify/skills/opencode/references/add-watch.md b/graphify/skills/opencode/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/opencode/references/add-watch.md +++ b/graphify/skills/opencode/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/opencode/references/exports.md b/graphify/skills/opencode/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/opencode/references/exports.md +++ b/graphify/skills/opencode/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/opencode/references/query.md b/graphify/skills/opencode/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/opencode/references/query.md +++ b/graphify/skills/opencode/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/opencode/references/transcribe.md b/graphify/skills/opencode/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/opencode/references/transcribe.md +++ b/graphify/skills/opencode/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/opencode/references/update.md b/graphify/skills/opencode/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/opencode/references/update.md +++ b/graphify/skills/opencode/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/graphify/skills/pi/references/add-watch.md b/graphify/skills/pi/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/pi/references/add-watch.md +++ b/graphify/skills/pi/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/pi/references/exports.md b/graphify/skills/pi/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/pi/references/exports.md +++ b/graphify/skills/pi/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/pi/references/query.md b/graphify/skills/pi/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/pi/references/query.md +++ b/graphify/skills/pi/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/pi/references/transcribe.md b/graphify/skills/pi/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/pi/references/transcribe.md +++ b/graphify/skills/pi/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/pi/references/update.md b/graphify/skills/pi/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/pi/references/update.md +++ b/graphify/skills/pi/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/graphify/skills/trae/references/add-watch.md b/graphify/skills/trae/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/trae/references/add-watch.md +++ b/graphify/skills/trae/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/trae/references/exports.md b/graphify/skills/trae/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/trae/references/exports.md +++ b/graphify/skills/trae/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/trae/references/query.md b/graphify/skills/trae/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/trae/references/query.md +++ b/graphify/skills/trae/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/trae/references/transcribe.md b/graphify/skills/trae/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/trae/references/transcribe.md +++ b/graphify/skills/trae/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/trae/references/update.md b/graphify/skills/trae/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/trae/references/update.md +++ b/graphify/skills/trae/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/graphify/skills/vscode/references/add-watch.md b/graphify/skills/vscode/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/vscode/references/add-watch.md +++ b/graphify/skills/vscode/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/vscode/references/exports.md b/graphify/skills/vscode/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/vscode/references/exports.md +++ b/graphify/skills/vscode/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/vscode/references/query.md b/graphify/skills/vscode/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/vscode/references/query.md +++ b/graphify/skills/vscode/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/vscode/references/transcribe.md b/graphify/skills/vscode/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/vscode/references/transcribe.md +++ b/graphify/skills/vscode/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/vscode/references/update.md b/graphify/skills/vscode/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/vscode/references/update.md +++ b/graphify/skills/vscode/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/graphify/skills/windows/references/add-watch.md b/graphify/skills/windows/references/add-watch.md index 77844343e1..1067a9560c 100644 --- a/graphify/skills/windows/references/add-watch.md +++ b/graphify/skills/windows/references/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/graphify/skills/windows/references/exports.md b/graphify/skills/windows/references/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/graphify/skills/windows/references/exports.md +++ b/graphify/skills/windows/references/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/graphify/skills/windows/references/query.md b/graphify/skills/windows/references/query.md index 56565eb782..b8bc4b5121 100644 --- a/graphify/skills/windows/references/query.md +++ b/graphify/skills/windows/references/query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/graphify/skills/windows/references/transcribe.md b/graphify/skills/windows/references/transcribe.md index b967f83799..1a08046668 100644 --- a/graphify/skills/windows/references/transcribe.md +++ b/graphify/skills/windows/references/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/graphify/skills/windows/references/update.md b/graphify/skills/windows/references/update.md index 3632fd4126..e5f20d1500 100644 --- a/graphify/skills/windows/references/update.md +++ b/graphify/skills/windows/references/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__agents__references__add-watch.md b/tools/skillgen/expected/graphify__skills__agents__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__agents__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__agents__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__agents__references__exports.md b/tools/skillgen/expected/graphify__skills__agents__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__agents__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__agents__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__agents__references__query.md b/tools/skillgen/expected/graphify__skills__agents__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__agents__references__query.md +++ b/tools/skillgen/expected/graphify__skills__agents__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__agents__references__transcribe.md b/tools/skillgen/expected/graphify__skills__agents__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__agents__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__agents__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__agents__references__update.md b/tools/skillgen/expected/graphify__skills__agents__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__agents__references__update.md +++ b/tools/skillgen/expected/graphify__skills__agents__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__amp__references__add-watch.md b/tools/skillgen/expected/graphify__skills__amp__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__amp__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__amp__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__amp__references__exports.md b/tools/skillgen/expected/graphify__skills__amp__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__amp__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__amp__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__amp__references__query.md b/tools/skillgen/expected/graphify__skills__amp__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__amp__references__query.md +++ b/tools/skillgen/expected/graphify__skills__amp__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__amp__references__transcribe.md b/tools/skillgen/expected/graphify__skills__amp__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__amp__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__amp__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__amp__references__update.md b/tools/skillgen/expected/graphify__skills__amp__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__amp__references__update.md +++ b/tools/skillgen/expected/graphify__skills__amp__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__claude__references__add-watch.md b/tools/skillgen/expected/graphify__skills__claude__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__claude__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__claude__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__claude__references__exports.md b/tools/skillgen/expected/graphify__skills__claude__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__claude__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__claude__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__claude__references__query.md b/tools/skillgen/expected/graphify__skills__claude__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__claude__references__query.md +++ b/tools/skillgen/expected/graphify__skills__claude__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__claude__references__transcribe.md b/tools/skillgen/expected/graphify__skills__claude__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__claude__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__claude__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__claude__references__update.md b/tools/skillgen/expected/graphify__skills__claude__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__claude__references__update.md +++ b/tools/skillgen/expected/graphify__skills__claude__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__claw__references__add-watch.md b/tools/skillgen/expected/graphify__skills__claw__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__claw__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__claw__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__claw__references__exports.md b/tools/skillgen/expected/graphify__skills__claw__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__claw__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__claw__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__claw__references__query.md b/tools/skillgen/expected/graphify__skills__claw__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__claw__references__query.md +++ b/tools/skillgen/expected/graphify__skills__claw__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__claw__references__transcribe.md b/tools/skillgen/expected/graphify__skills__claw__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__claw__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__claw__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__claw__references__update.md b/tools/skillgen/expected/graphify__skills__claw__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__claw__references__update.md +++ b/tools/skillgen/expected/graphify__skills__claw__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__codex__references__add-watch.md b/tools/skillgen/expected/graphify__skills__codex__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__codex__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__codex__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__codex__references__exports.md b/tools/skillgen/expected/graphify__skills__codex__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__codex__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__codex__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__codex__references__query.md b/tools/skillgen/expected/graphify__skills__codex__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__codex__references__query.md +++ b/tools/skillgen/expected/graphify__skills__codex__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__codex__references__transcribe.md b/tools/skillgen/expected/graphify__skills__codex__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__codex__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__codex__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__codex__references__update.md b/tools/skillgen/expected/graphify__skills__codex__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__codex__references__update.md +++ b/tools/skillgen/expected/graphify__skills__codex__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__copilot__references__add-watch.md b/tools/skillgen/expected/graphify__skills__copilot__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__copilot__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__copilot__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__copilot__references__exports.md b/tools/skillgen/expected/graphify__skills__copilot__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__copilot__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__copilot__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__copilot__references__query.md b/tools/skillgen/expected/graphify__skills__copilot__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__copilot__references__query.md +++ b/tools/skillgen/expected/graphify__skills__copilot__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__copilot__references__transcribe.md b/tools/skillgen/expected/graphify__skills__copilot__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__copilot__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__copilot__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__copilot__references__update.md b/tools/skillgen/expected/graphify__skills__copilot__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__copilot__references__update.md +++ b/tools/skillgen/expected/graphify__skills__copilot__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__droid__references__add-watch.md b/tools/skillgen/expected/graphify__skills__droid__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__droid__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__droid__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__droid__references__exports.md b/tools/skillgen/expected/graphify__skills__droid__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__droid__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__droid__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__droid__references__query.md b/tools/skillgen/expected/graphify__skills__droid__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__droid__references__query.md +++ b/tools/skillgen/expected/graphify__skills__droid__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__droid__references__transcribe.md b/tools/skillgen/expected/graphify__skills__droid__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__droid__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__droid__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__droid__references__update.md b/tools/skillgen/expected/graphify__skills__droid__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__droid__references__update.md +++ b/tools/skillgen/expected/graphify__skills__droid__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__kilo__references__add-watch.md b/tools/skillgen/expected/graphify__skills__kilo__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__kilo__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__kilo__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__kilo__references__exports.md b/tools/skillgen/expected/graphify__skills__kilo__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__kilo__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__kilo__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__kilo__references__query.md b/tools/skillgen/expected/graphify__skills__kilo__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__kilo__references__query.md +++ b/tools/skillgen/expected/graphify__skills__kilo__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__kilo__references__transcribe.md b/tools/skillgen/expected/graphify__skills__kilo__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__kilo__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__kilo__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__kilo__references__update.md b/tools/skillgen/expected/graphify__skills__kilo__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__kilo__references__update.md +++ b/tools/skillgen/expected/graphify__skills__kilo__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__kiro__references__add-watch.md b/tools/skillgen/expected/graphify__skills__kiro__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__kiro__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__kiro__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__kiro__references__exports.md b/tools/skillgen/expected/graphify__skills__kiro__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__kiro__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__kiro__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__kiro__references__query.md b/tools/skillgen/expected/graphify__skills__kiro__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__kiro__references__query.md +++ b/tools/skillgen/expected/graphify__skills__kiro__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__kiro__references__transcribe.md b/tools/skillgen/expected/graphify__skills__kiro__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__kiro__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__kiro__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__kiro__references__update.md b/tools/skillgen/expected/graphify__skills__kiro__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__kiro__references__update.md +++ b/tools/skillgen/expected/graphify__skills__kiro__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__opencode__references__add-watch.md b/tools/skillgen/expected/graphify__skills__opencode__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__opencode__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__opencode__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__opencode__references__exports.md b/tools/skillgen/expected/graphify__skills__opencode__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__opencode__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__opencode__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__opencode__references__query.md b/tools/skillgen/expected/graphify__skills__opencode__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__opencode__references__query.md +++ b/tools/skillgen/expected/graphify__skills__opencode__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__opencode__references__transcribe.md b/tools/skillgen/expected/graphify__skills__opencode__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__opencode__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__opencode__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__opencode__references__update.md b/tools/skillgen/expected/graphify__skills__opencode__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__opencode__references__update.md +++ b/tools/skillgen/expected/graphify__skills__opencode__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__pi__references__add-watch.md b/tools/skillgen/expected/graphify__skills__pi__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__pi__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__pi__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__pi__references__exports.md b/tools/skillgen/expected/graphify__skills__pi__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__pi__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__pi__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__pi__references__query.md b/tools/skillgen/expected/graphify__skills__pi__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__pi__references__query.md +++ b/tools/skillgen/expected/graphify__skills__pi__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__pi__references__transcribe.md b/tools/skillgen/expected/graphify__skills__pi__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__pi__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__pi__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__pi__references__update.md b/tools/skillgen/expected/graphify__skills__pi__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__pi__references__update.md +++ b/tools/skillgen/expected/graphify__skills__pi__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__trae__references__add-watch.md b/tools/skillgen/expected/graphify__skills__trae__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__trae__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__trae__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__trae__references__exports.md b/tools/skillgen/expected/graphify__skills__trae__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__trae__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__trae__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__trae__references__query.md b/tools/skillgen/expected/graphify__skills__trae__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__trae__references__query.md +++ b/tools/skillgen/expected/graphify__skills__trae__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__trae__references__transcribe.md b/tools/skillgen/expected/graphify__skills__trae__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__trae__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__trae__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__trae__references__update.md b/tools/skillgen/expected/graphify__skills__trae__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__trae__references__update.md +++ b/tools/skillgen/expected/graphify__skills__trae__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__vscode__references__add-watch.md b/tools/skillgen/expected/graphify__skills__vscode__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__vscode__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__vscode__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__vscode__references__exports.md b/tools/skillgen/expected/graphify__skills__vscode__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__vscode__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__vscode__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__vscode__references__query.md b/tools/skillgen/expected/graphify__skills__vscode__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__vscode__references__query.md +++ b/tools/skillgen/expected/graphify__skills__vscode__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__vscode__references__transcribe.md b/tools/skillgen/expected/graphify__skills__vscode__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__vscode__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__vscode__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__vscode__references__update.md b/tools/skillgen/expected/graphify__skills__vscode__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__vscode__references__update.md +++ b/tools/skillgen/expected/graphify__skills__vscode__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/expected/graphify__skills__windows__references__add-watch.md b/tools/skillgen/expected/graphify__skills__windows__references__add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/expected/graphify__skills__windows__references__add-watch.md +++ b/tools/skillgen/expected/graphify__skills__windows__references__add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/expected/graphify__skills__windows__references__exports.md b/tools/skillgen/expected/graphify__skills__windows__references__exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/expected/graphify__skills__windows__references__exports.md +++ b/tools/skillgen/expected/graphify__skills__windows__references__exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/expected/graphify__skills__windows__references__query.md b/tools/skillgen/expected/graphify__skills__windows__references__query.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/expected/graphify__skills__windows__references__query.md +++ b/tools/skillgen/expected/graphify__skills__windows__references__query.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/expected/graphify__skills__windows__references__transcribe.md b/tools/skillgen/expected/graphify__skills__windows__references__transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/expected/graphify__skills__windows__references__transcribe.md +++ b/tools/skillgen/expected/graphify__skills__windows__references__transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/expected/graphify__skills__windows__references__update.md b/tools/skillgen/expected/graphify__skills__windows__references__update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/expected/graphify__skills__windows__references__update.md +++ b/tools/skillgen/expected/graphify__skills__windows__references__update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json diff --git a/tools/skillgen/fragments/references/query/default.md b/tools/skillgen/fragments/references/query/default.md index 56565eb782..b8bc4b5121 100644 --- a/tools/skillgen/fragments/references/query/default.md +++ b/tools/skillgen/fragments/references/query/default.md @@ -11,7 +11,8 @@ Two traversal modes - choose based on the question: First check the graph exists: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " from pathlib import Path if not Path('graphify-out/graph.json').exists(): print('ERROR: No graph found. Run /graphify first to build the graph.') @@ -28,7 +29,8 @@ Fix this **without inventing tokens** by expanding the query against the actual 1. Extract the token vocabulary from node labels: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, re from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) @@ -64,8 +66,8 @@ Build the **expanded query string** by joining the selected tokens with spaces. Prefer the CLI when it is installed: ```bash -graphify query "QUESTION" -# or: graphify query "QUESTION" --dfs --budget 3000 +graphify query 'QUESTION' +# or: graphify query 'QUESTION' --dfs --budget 3000 ``` If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline: @@ -77,7 +79,8 @@ If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal 5. If the graph lacks enough information, say so - do not hallucinate edges. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from networkx.readwrite import json_graph import networkx as nx @@ -86,8 +89,8 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -question = 'QUESTION' -mode = 'MODE' # 'bfs' or 'dfs' +question = sys.argv[1] +mode = sys.argv[2] # 'bfs' or 'dfs' terms = [t.lower() for t in question.split() if len(t) >= 3] # match the vocab threshold; keeps api/jwt/ios (#1392) # Find best-matching start nodes @@ -137,7 +140,10 @@ else: frontier = next_frontier # Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) -token_budget = BUDGET # default 2000 +try: + token_budget = int(sys.argv[3]) +except (IndexError, ValueError): + token_budget = 2000 char_budget = token_budget * 4 # Score each node by term overlap for ranked output @@ -160,18 +166,19 @@ output = '\n'.join(lines) if len(output) > char_budget: output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' print(output) -" +" 'QUESTION' 'MODE' 'BUDGET' ``` -Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains. +Pass `QUESTION`, `MODE`, and `BUDGET` as single-quoted shell arguments after the Python source. Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then answer based on the subgraph output above, using only what the graph contains. After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node: ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'ORIGINAL_QUESTION' --answer 'ANSWER' --type query --nodes 'NODE1' 'NODE2' ``` -Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. +Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. The single quotes around every user-supplied value (including each `--nodes` argument) make the substitution injection-safe: a question like `hello"; rm -rf /` or a node label containing whitespace, single quotes, or shell metacharacters cannot escape the argument. If `ORIGINAL_QUESTION`, `ANSWER`, or any node label contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote) - the escape applies to every substituted value, not just node labels. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. **Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting): @@ -188,13 +195,14 @@ At the **start** of graph work, refresh and read the lessons: run `graphify refl Find the shortest path between two named concepts in the graph. Prefer the CLI when installed: ```bash -graphify path "NODE_A" "NODE_B" +graphify path 'NODE_A' 'NODE_B' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -203,9 +211,6 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -a_term = 'NODE_A' -b_term = 'NODE_B' - def find_node(term): term = term.lower() scored = sorted( @@ -215,6 +220,9 @@ def find_node(term): ) return scored[0][1] if scored and scored[0][0] > 0 else None +a_term = sys.argv[1] +b_term = sys.argv[2] + src = find_node(a_term) tgt = find_node(b_term) @@ -238,15 +246,16 @@ except nx.NetworkXNoPath: print(f'No path found between {a_term!r} and {b_term!r}') except nx.NodeNotFound as e: print(f'Node not found: {e}') -" +" 'NODE_A' 'NODE_B' ``` -Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. +Pass `NODE_A` and `NODE_B` as single-quoted shell arguments after the Python source. Replace them with the actual concept names from the user. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then explain the path in plain language - what each hop means, why it's significant. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Path from NODE_A to NODE_B' --answer 'ANSWER' --type path_query --nodes 'NODE_A' 'NODE_B' ``` --- @@ -256,13 +265,14 @@ $(cat graphify-out/.graphify_python) -m graphify save-result --question "Path fr Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed: ```bash -graphify explain "NODE_NAME" +graphify explain 'NODE_NAME' ``` If the CLI is unavailable, run it inline: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, sys import networkx as nx from networkx.readwrite import json_graph @@ -271,7 +281,7 @@ from pathlib import Path data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) G = json_graph.node_link_graph(data, edges='links') -term = 'NODE_NAME' +term = sys.argv[1] term_lower = term.lower() # Find best matching node @@ -299,13 +309,14 @@ for neighbor in G.neighbors(nid): conf = edge.get('confidence', '') src_file = G.nodes[neighbor].get('source_file', '') print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') -" +" 'NODE_NAME' ``` -Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. +Pass `NODE_NAME` as a single-quoted shell argument after the Python source. Replace it with the concept the user asked about. The single quotes keep the substitution injection-safe: any value containing a literal single quote must have it replaced with `'\''` (close-quote, escaped-quote, open-quote). Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. -After writing the explanation, save it back: +After writing the explanation, save it back (single quotes around the user-supplied values keep the substitution injection-safe; replace any literal `'` in the substituted text with `'\''`): ```bash -$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify save-result --question 'Explain NODE_NAME' --answer 'ANSWER' --type explain --nodes 'NODE_NAME' ``` diff --git a/tools/skillgen/fragments/references/shared/add-watch.md b/tools/skillgen/fragments/references/shared/add-watch.md index 77844343e1..1067a9560c 100644 --- a/tools/skillgen/fragments/references/shared/add-watch.md +++ b/tools/skillgen/fragments/references/shared/add-watch.md @@ -7,13 +7,18 @@ Load this when the user ran `/graphify add ` or passed `--watch`. Neither i Fetch a URL and add it to the corpus, then update the graph. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys from graphify.ingest import ingest from pathlib import Path +url = sys.argv[1] +author = sys.argv[2] if len(sys.argv) > 2 else '' +contributor = sys.argv[3] if len(sys.argv) > 3 else '' + try: - out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + out = ingest(url, Path('./raw'), author=author, contributor=contributor) print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) @@ -21,10 +26,10 @@ except ValueError as e: except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) -" +" 'URL' 'AUTHOR' 'CONTRIBUTOR' ``` -Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. Each value is passed as a separate shell argument and read in Python via `sys.argv`, so user-supplied text never lands inside the evaluated Python source - even a URL containing single quotes, double quotes, backticks, or shell metacharacters is treated as a string value, not code. Single-quote each argument in the shell command; if a value contains a literal single quote, replace it with `'\''` (close-quote, escaped-quote, open-quote). If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. Supported URL types (auto-detected): - YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) @@ -41,7 +46,8 @@ Supported URL types (auto-detected): Start a background watcher that monitors a folder and auto-updates the graph when files change. ```bash -$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3 +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.watch INPUT_PATH --debounce 3 ``` Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: diff --git a/tools/skillgen/fragments/references/shared/exports.md b/tools/skillgen/fragments/references/shared/exports.md index 242ff868e0..7a2aa30b89 100644 --- a/tools/skillgen/fragments/references/shared/exports.md +++ b/tools/skillgen/fragments/references/shared/exports.md @@ -59,7 +59,8 @@ graphify export graphml ### Step 7d - MCP server (only if --mcp flag) ```bash -$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -m graphify.serve graphify-out/graph.json ``` This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. diff --git a/tools/skillgen/fragments/references/shared/transcribe.md b/tools/skillgen/fragments/references/shared/transcribe.md index b967f83799..1a08046668 100644 --- a/tools/skillgen/fragments/references/shared/transcribe.md +++ b/tools/skillgen/fragments/references/shared/transcribe.md @@ -25,8 +25,13 @@ Read the top god node labels from detect output or analysis, then compose a shor ```bash export GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed (must be exported) -export GRAPHIFY_WHISPER_PROMPT="" -$(cat graphify-out/.graphify_python) -c " +# Single quotes keep the prompt literal — no expansion of $, `, or \ inside it. +# If your domain hint itself contains a single quote, replace it with '\'' (close-quote, escaped-quote, open-quote) +# or use a here-doc / printf %q pattern; the export must still happen for the child Python process to inherit it. +GRAPHIFY_WHISPER_PROMPT='' +export GRAPHIFY_WHISPER_PROMPT +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json, os, sys from pathlib import Path from graphify.transcribe import transcribe_all diff --git a/tools/skillgen/fragments/references/shared/update.md b/tools/skillgen/fragments/references/shared/update.md index 3632fd4126..e5f20d1500 100644 --- a/tools/skillgen/fragments/references/shared/update.md +++ b/tools/skillgen/fragments/references/shared/update.md @@ -7,7 +7,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path @@ -30,7 +31,8 @@ if new_total > 0: Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) @@ -48,7 +50,8 @@ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ If new files exist, first check whether all changed files are code files: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path @@ -71,7 +74,8 @@ If no new files exist (only deletions), create an empty extraction so the merge ```bash if [ ! -f graphify-out/.graphify_extract.json ]; then echo '[graphify update] Only deletions -- creating empty extraction for merge.' - $(cat graphify-out/.graphify_python) -c " + readarray -t GFY_PYTHON < graphify-out/.graphify_python + "${GFY_PYTHON[@]}" -c " import json from pathlib import Path Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') @@ -83,7 +87,8 @@ fi Then: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from pathlib import Path from graphify.build import build_merge @@ -170,7 +175,8 @@ Then run Steps 4–8 on the merged graph as normal. After Step 4, show the graph diff: ```bash -$(cat graphify-out/.graphify_python) -c " +readarray -t GFY_PYTHON < graphify-out/.graphify_python +"${GFY_PYTHON[@]}" -c " import json from graphify.analyze import graph_diff from graphify.build import build_from_json