1bcoder 0.1.14__tar.gz → 0.1.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. {1bcoder-0.1.14 → 1bcoder-0.1.16}/1bcoder.egg-info/PKG-INFO +1 -1
  2. {1bcoder-0.1.14 → 1bcoder-0.1.16}/1bcoder.egg-info/SOURCES.txt +3 -0
  3. {1bcoder-0.1.14 → 1bcoder-0.1.16}/PKG-INFO +1 -1
  4. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/agents/websearch.txt +7 -1
  5. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/doc/PARAM.md +4 -0
  6. 1bcoder-0.1.16/_bcoder_data/flows/compress.py +310 -0
  7. 1bcoder-0.1.16/_bcoder_data/flows/deepagent_md.py +875 -0
  8. 1bcoder-0.1.16/_bcoder_data/proc/ctx_expand.py +37 -0
  9. {1bcoder-0.1.14 → 1bcoder-0.1.16}/chat.py +303 -27
  10. {1bcoder-0.1.14 → 1bcoder-0.1.16}/pyproject.toml +1 -1
  11. {1bcoder-0.1.14 → 1bcoder-0.1.16}/1bcoder.egg-info/dependency_links.txt +0 -0
  12. {1bcoder-0.1.14 → 1bcoder-0.1.16}/1bcoder.egg-info/entry_points.txt +0 -0
  13. {1bcoder-0.1.14 → 1bcoder-0.1.16}/1bcoder.egg-info/requires.txt +0 -0
  14. {1bcoder-0.1.14 → 1bcoder-0.1.16}/1bcoder.egg-info/top_level.txt +0 -0
  15. {1bcoder-0.1.14 → 1bcoder-0.1.16}/LICENSE +0 -0
  16. {1bcoder-0.1.14 → 1bcoder-0.1.16}/README.md +0 -0
  17. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/__init__.py +0 -0
  18. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/agents/advance.txt +0 -0
  19. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/agents/ask.txt +0 -0
  20. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/agents/compact.txt +0 -0
  21. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/agents/concepts.txt +0 -0
  22. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/agents/fill.txt +0 -0
  23. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/agents/planning.txt +0 -0
  24. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/agents/scan.txt +0 -0
  25. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/agents/sqlite.txt +0 -0
  26. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/aliases.txt +0 -0
  27. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/doc/FLOWS.md +0 -0
  28. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/doc/MCP.md +0 -0
  29. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/doc/OLLAMA_SERVER_PARAM.md +0 -0
  30. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/doc/PROC.md +0 -0
  31. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/doc/TRANSLATE.md +0 -0
  32. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/flows/__pycache__/commit_message.cpython-311.pyc +0 -0
  33. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/flows/__pycache__/webcrawl.cpython-311.pyc +0 -0
  34. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/flows/commit_message.py +0 -0
  35. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/flows/deepagent.py +0 -0
  36. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/flows/deobfuscate.py +0 -0
  37. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/flows/external_help.py +0 -0
  38. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/flows/grounding.py +0 -0
  39. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/flows/obfuscate.py +0 -0
  40. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/flows/py_error_trace.py +0 -0
  41. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/flows/simargl_files.py +0 -0
  42. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/flows/visual_search.py +0 -0
  43. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/flows/webask.py +0 -0
  44. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/flows/webcrawl.py +0 -0
  45. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/map.txt +0 -0
  46. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/action-required.py +0 -0
  47. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/add-save.py +0 -0
  48. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/assist.py +0 -0
  49. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/collect-files.py +0 -0
  50. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/ctx_cut.py +0 -0
  51. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/extract-code.py +0 -0
  52. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/extract-files.py +0 -0
  53. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/extract-list.py +0 -0
  54. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/grounding-check.py +0 -0
  55. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/md.py +0 -0
  56. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/mdx.py +0 -0
  57. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/pattern-gate.py +0 -0
  58. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/regexp-extract.py +0 -0
  59. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/rude_words.py +0 -0
  60. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/scan-save.py +0 -0
  61. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/secret_check.py +0 -0
  62. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/sql_readonly_guard.py +0 -0
  63. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/proc/tempctx-cut.py +0 -0
  64. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/profiles.txt +0 -0
  65. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/prompts/analysis.txt +0 -0
  66. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/prompts/sumarise.txt +0 -0
  67. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/prompts.txt +0 -0
  68. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/AddFunction.txt +0 -0
  69. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/AskProject.txt +0 -0
  70. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/CheckRequirements.txt +0 -0
  71. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/DockerMySQL.txt +0 -0
  72. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/DockerNginx.txt +0 -0
  73. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/DockerPython.txt +0 -0
  74. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/DockerStack.txt +0 -0
  75. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/DuckDuckGoInstant.txt +0 -0
  76. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/EnvTemplate.txt +0 -0
  77. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/Explain.txt +0 -0
  78. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/ExploreProjectStructure.txt +0 -0
  79. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/GitIgnorePython.txt +0 -0
  80. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/MySQLDump.txt +0 -0
  81. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/NewScript.txt +0 -0
  82. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/PipFreeze.txt +0 -0
  83. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/PyPI.txt +0 -0
  84. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/Refactor.txt +0 -0
  85. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/RunAndFix.txt +0 -0
  86. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/SQLiteSchema.txt +0 -0
  87. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/Translate.txt +0 -0
  88. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/WikiPage.txt +0 -0
  89. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/WikiSearch.txt +0 -0
  90. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/auto-bkup.txt +0 -0
  91. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/edit-control.txt +0 -0
  92. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/parallel_call.txt +0 -0
  93. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/personal/content/create-regular-content.txt +0 -0
  94. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/personal/content/plan.txt +0 -0
  95. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/personal/test/collect-data-from-test-environment.txt +0 -0
  96. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/plan.txt +0 -0
  97. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/remote/create-content-on-remote-server.txt +0 -0
  98. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/set_ctx.txt +0 -0
  99. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/simargl-cli_index_files.txt +0 -0
  100. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/simargl-cli_index_units.txt +0 -0
  101. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/simargl-cli_search.txt +0 -0
  102. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/team-map-worker.txt +0 -0
  103. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/team-search-worker.txt +0 -0
  104. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/team-summarize.txt +0 -0
  105. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/team-tree-worker.txt +0 -0
  106. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/scripts/test.txt +0 -0
  107. {1bcoder-0.1.14 → 1bcoder-0.1.16}/_bcoder_data/teams/code-analysis.yaml +0 -0
  108. {1bcoder-0.1.14 → 1bcoder-0.1.16}/map_index.py +0 -0
  109. {1bcoder-0.1.14 → 1bcoder-0.1.16}/map_query.py +0 -0
  110. {1bcoder-0.1.14 → 1bcoder-0.1.16}/setup.cfg +0 -0
  111. {1bcoder-0.1.14 → 1bcoder-0.1.16}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: 1bcoder
3
- Version: 0.1.14
3
+ Version: 0.1.16
4
4
  Summary: AI coding assistant agent for 1B–7B local models (Ollama, LMStudio, llama.cpp). Terminal REPL with file editing, project map, agents, scripts, and parallel multi-model queries.
5
5
  Project-URL: Homepage, https://github.com/szholobetsky/1bcoder
6
6
  Project-URL: Repository, https://github.com/szholobetsky/1bcoder
@@ -31,7 +31,9 @@ _bcoder_data/doc/PARAM.md
31
31
  _bcoder_data/doc/PROC.md
32
32
  _bcoder_data/doc/TRANSLATE.md
33
33
  _bcoder_data/flows/commit_message.py
34
+ _bcoder_data/flows/compress.py
34
35
  _bcoder_data/flows/deepagent.py
36
+ _bcoder_data/flows/deepagent_md.py
35
37
  _bcoder_data/flows/deobfuscate.py
36
38
  _bcoder_data/flows/external_help.py
37
39
  _bcoder_data/flows/grounding.py
@@ -48,6 +50,7 @@ _bcoder_data/proc/add-save.py
48
50
  _bcoder_data/proc/assist.py
49
51
  _bcoder_data/proc/collect-files.py
50
52
  _bcoder_data/proc/ctx_cut.py
53
+ _bcoder_data/proc/ctx_expand.py
51
54
  _bcoder_data/proc/extract-code.py
52
55
  _bcoder_data/proc/extract-files.py
53
56
  _bcoder_data/proc/extract-list.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: 1bcoder
3
- Version: 0.1.14
3
+ Version: 0.1.16
4
4
  Summary: AI coding assistant agent for 1B–7B local models (Ollama, LMStudio, llama.cpp). Terminal REPL with file editing, project map, agents, scripts, and parallel multi-model queries.
5
5
  Project-URL: Homepage, https://github.com/szholobetsky/1bcoder
6
6
  Project-URL: Repository, https://github.com/szholobetsky/1bcoder
@@ -13,8 +13,14 @@ system =
13
13
  One ACTION per turn.
14
14
  When done, write a clear summary answer. Do not write any ACTION when done.
15
15
 
16
+ IMPORTANT: never wrap the /web search query in quotes. Write keywords as plain words.
17
+ Wrong: ACTION: /web search "lenovo m75q gen 2 price ukraine"
18
+ Right: ACTION: /web search lenovo m75q gen 2 price ukraine
19
+
16
20
  Strategy:
17
- 1. Start with ACTION: /web search <question>
21
+ 1. Start with ACTION: /web search keyword1 keyword2 keyword*
22
+ DuckDuckGo operators: "exact phrase", -exclude, OR, AND, site:, filetype:, intitle:, inurl:, region:
23
+ Use quotes only around specific phrases, not around the whole query.
18
24
  2. Review the search results (titles, URLs, snippets)
19
25
  3. Pick the most relevant URLs and fetch them with ACTION: /web fetch <url>
20
26
  4. Fetch up to 3 pages total — stop earlier if you already have enough to answer
@@ -19,6 +19,7 @@ These are handled by 1bcoder directly and are not forwarded to the model API.
19
19
  | `ask_show` | 500 | Characters shown in terminal when output is truncated. |
20
20
  | `run_timeout` | 30 | Timeout in seconds for `/run` shell commands. Set to `0` to disable (no timeout). Useful for long-running CLI tools like `simargl search`. |
21
21
  | `log` | `false` | Print full request details before each LLM call: URL, model, message count, options. Also prints HTTP response headers on error. Useful for debugging 500 errors or unexpected model behaviour. |
22
+ | `keep_alive` | *(not set)* | **Ollama only.** How long to keep the model loaded after a response. `-1` = never unload. `"5m"` = 5 minutes (Ollama default). `"0"` = unload immediately. Set `-1` to prevent KV-cache eviction during long sessions. |
22
23
 
23
24
  ---
24
25
 
@@ -134,6 +135,9 @@ An alternative to top_p/top_k that targets a specific perplexity level.
134
135
  # Keep reasoning in context for chained agents
135
136
  /param think_exclude false
136
137
 
138
+ # Prevent Ollama from evicting KV-cache between requests (fixes slowdown after 10k+ tokens)
139
+ /param keep_alive -1
140
+
137
141
  # Reset everything
138
142
  /param clear
139
143
  ```
@@ -0,0 +1,310 @@
1
+ """compress — annotate and remove redundant words/phrases.
2
+
3
+ Four modes:
4
+ --mode rules pure rule-based (fast, deterministic, covers common patterns)
5
+ --mode list LLM outputs a PHRASE → reason list, then annotate (default for hybrid step)
6
+ --mode inline LLM rewrites text with <phrase|reason> tags inline
7
+ --mode hybrid rules first, then list-model for remaining text (default)
8
+
9
+ Usage:
10
+ /flow compress <text>
11
+ /flow compress $ (compress last LLM reply)
12
+ /flow compress file: notes.txt
13
+ /flow compress --strip <text> (output compressed only, no annotations)
14
+ /flow compress --mode rules <text>
15
+ /flow compress --mode list <text>
16
+ /flow compress --mode inline <text>
17
+ """
18
+ import re as _re
19
+ import os as _os
20
+
21
+ # ── rule-based patterns ───────────────────────────────────────────────────────
22
+
23
+ _RULES = [
24
+ # hedges
25
+ (r"\bI think,?\s*", "hedge"),
26
+ (r"\bI believe,?\s*", "hedge"),
27
+ (r"\bI feel,?\s*", "hedge"),
28
+ (r"\bin my opinion,?\s*", "hedge"),
29
+ (r"\bit seems(?: that)?,?\s*","hedge"),
30
+ (r"\bperhaps\s+", "hedge"),
31
+ (r"\bmaybe\s+", "hedge"),
32
+ (r"\bprobably\s+", "hedge"),
33
+ (r"\bgenerally\s+", "hedge"),
34
+ (r"\busually\s+", "hedge"),
35
+ (r"\btypically\s+", "hedge"),
36
+ # filler intensifiers
37
+ (r"\bvery\s+", "filler"),
38
+ (r"\bquite\s+", "filler"),
39
+ (r"\brather\s+", "filler"),
40
+ (r"\breally\s+", "filler"),
41
+ (r"\bextremely\s+", "filler"),
42
+ (r"\babsolutely\s+", "filler"),
43
+ (r"\bcompletely\s+", "filler"),
44
+ (r"\btotally\s+", "filler"),
45
+ (r"\bsomewhat\s+", "filler"),
46
+ # empty phrases
47
+ (r"\bthe fact that\s+", "filler"),
48
+ (r"\bit is worth noting that\s+", "filler"),
49
+ (r"\bit should be noted that\s+", "filler"),
50
+ (r"\bit is important to note that\s+", "filler"),
51
+ (r"\bit is worth mentioning that\s+", "filler"),
52
+ (r"\bit is necessary to note that\s+", "filler"),
53
+ (r"\bnote that\s+", "filler"),
54
+ (r"\bin order to\s+", "filler"),
55
+ (r"\bI would like to take this opportunity to mention\s+that\s+", "hedge"),
56
+ (r"\bI would like to take this opportunity to\s+", "hedge"),
57
+ (r"\btake this opportunity to\s+", "hedge"),
58
+ (r"\bdue to the fact that\s+","filler"),
59
+ (r"\bas a matter of fact,?\s*","filler"),
60
+ (r"\bbasically\s+", "filler"),
61
+ (r"\bessentially\s+", "filler"),
62
+ (r"\bfundamentally\s+", "filler"),
63
+ # weak verb clusters
64
+ (r"\btends? to\s+", "weaken"),
65
+ (r"\bseems? to\s+", "weaken"),
66
+ (r"\bappears? to\s+", "weaken"),
67
+ # redundant quantifiers
68
+ (r"\beach and every\b", "redundant"),
69
+ (r"\bfirst and foremost\b", "redundant"),
70
+ (r"\bat this point in time\b","redundant"),
71
+ (r"\bin the event that\b", "redundant"),
72
+ ]
73
+
74
+ _STRIP_RE = _re.compile(r'<([^|>]+)\|[^>]+>')
75
+ _INLINE_RE = _re.compile(r'<([^|>]+)\|([a-z]+)>')
76
+
77
+ _PROMPT_LIST = """\
78
+ Find redundant words and phrases in the text below.
79
+ Output ONLY a list, one per line: PHRASE → reason
80
+ Reasons: hedge, filler, redundant, weaken
81
+
82
+ Rules:
83
+ - PHRASE must be copied exactly from the text
84
+ - Never mark: technical terms, subjects, main verbs, key nouns
85
+ - Only mark words that can be removed without changing the core meaning
86
+
87
+ Example:
88
+ Text: I think cats are very nice animals that people generally tend to like a lot
89
+ I think → hedge
90
+ very → filler
91
+ that people generally tend to like a lot → redundant
92
+
93
+ Text: {text}
94
+ """
95
+
96
+ _PROMPT_INLINE = """\
97
+ Rewrite the text below with redundant words and phrases wrapped in <word|reason> tags.
98
+ Reasons: hedge, filler, redundant, weaken
99
+ Keep ALL other words exactly as they are. Do not add, remove, or reorder any words outside the tags.
100
+
101
+ Rules:
102
+ - Never mark: technical terms, subjects, main verbs, key nouns
103
+ - Only mark what can be removed without changing the core meaning
104
+
105
+ Example:
106
+ Input: I think cats are very nice animals that people generally tend to like a lot
107
+ Output: <I think|hedge> cats are <very|filler> nice animals <that people generally tend to like a lot|redundant>
108
+
109
+ Input: {text}
110
+ Output:"""
111
+
112
+
113
+ # ── core functions ────────────────────────────────────────────────────────────
114
+
115
+ def _apply_rules(text: str) -> tuple:
116
+ """Returns (annotated_text, [(phrase, reason), ...]).
117
+ Searches all patterns in the ORIGINAL text, deduplicates, then annotates once."""
118
+ candidates = []
119
+ for pattern, reason in _RULES:
120
+ m = _re.search(pattern, text, _re.IGNORECASE)
121
+ if m:
122
+ candidates.append((m.group(0).rstrip(), reason))
123
+
124
+ # deduplicate: keep longest, remove any phrase that is a substring of a longer one
125
+ found = []
126
+ for phrase, reason in sorted(candidates, key=lambda x: -len(x[0])):
127
+ if not any(phrase.lower() in p.lower() for p, _ in found):
128
+ found.append((phrase, reason))
129
+
130
+ result = _annotate(text, found)
131
+ return _re.sub(r' +', ' ', result).strip(), found
132
+
133
+
134
+ _VALID_REASONS = {'hedge', 'filler', 'redundant', 'weaken'}
135
+
136
+ def _parse_model_list(raw: str) -> list:
137
+ pairs = []
138
+ for line in raw.splitlines():
139
+ line = line.strip()
140
+ # skip lines without a separator — they're prose/thinking output
141
+ sep_found = None
142
+ for sep in (' → ', ' -> ', ' — ', ': '):
143
+ if sep in line:
144
+ sep_found = sep
145
+ break
146
+ if not sep_found:
147
+ continue
148
+ phrase, _, reason = line.partition(sep_found)
149
+ # clean up list markers and stray quotes from phrase
150
+ phrase = phrase.strip()
151
+ phrase = _re.sub(r'^[-*•]\s*', '', phrase) # leading - * •
152
+ phrase = phrase.strip('"\'`') # surrounding quotes/backticks
153
+ phrase = phrase.strip()
154
+ reason = reason.strip().split()[0].lower().rstrip('.,;')
155
+ # only accept known reasons, skip malformed/thinking lines
156
+ if phrase and reason in _VALID_REASONS:
157
+ pairs.append((phrase, reason))
158
+ # deduplicate — keep first occurrence of each phrase
159
+ seen = set()
160
+ return [(p, r) for p, r in pairs if not (p in seen or seen.add(p))]
161
+
162
+
163
+ def _annotate(text: str, pairs: list) -> str:
164
+ result = text
165
+ for phrase, reason in sorted(pairs, key=lambda x: -len(x[0])):
166
+ result = _re.sub(_re.escape(phrase), f"<{phrase}|{reason}>", result, count=1)
167
+ return result
168
+
169
+
170
+ def _compress(annotated: str) -> str:
171
+ # strip annotations, handle nested cases by repeating until stable
172
+ text = annotated
173
+ for _ in range(5):
174
+ prev = text
175
+ text = _STRIP_RE.sub('', text)
176
+ # also clean up any leftover |reason> fragments from nesting
177
+ text = _re.sub(r'\|[a-z]+>', '', text)
178
+ text = _re.sub(r'<[^>]*>', '', text) # catch any remaining < > fragments
179
+ if text == prev:
180
+ break
181
+ text = _re.sub(r' +', ' ', text).strip()
182
+ # clean up orphan punctuation at start: ". text" → "text"
183
+ text = _re.sub(r'^[.,;:\-–—]+\s*', '', text)
184
+ return text
185
+
186
+
187
+ def _model_annotate_list(chat, text: str) -> tuple:
188
+ prompt = _PROMPT_LIST.format(text=text)
189
+ msgs = [
190
+ {"role": "system", "content": "You are a text editor. Output only the redundancy list."},
191
+ {"role": "user", "content": prompt},
192
+ ]
193
+ raw = chat._stream_chat(msgs) or ""
194
+ print()
195
+ pairs = _parse_model_list(raw)
196
+ return _annotate(text, pairs), pairs
197
+
198
+
199
+ def _model_annotate_inline(chat, text: str) -> tuple:
200
+ prompt = _PROMPT_INLINE.format(text=text)
201
+ msgs = [
202
+ {"role": "system", "content": "You are a text editor. Output only the rewritten line with tags."},
203
+ {"role": "user", "content": prompt},
204
+ ]
205
+ raw = chat._stream_chat(msgs) or ""
206
+ print()
207
+ pairs = [(p, r) for p, r in _INLINE_RE.findall(raw) if r in _VALID_REASONS]
208
+ seen: set = set()
209
+ pairs = [(p, r) for p, r in pairs if not (p in seen or seen.add(p))]
210
+ # raw is already annotated; clean up any stray leading/trailing prose
211
+ annotated = raw.strip().splitlines()[-1] if raw.strip() else text
212
+ return annotated, pairs
213
+
214
+
215
+ # ── main ──────────────────────────────────────────────────────────────────────
216
+
217
+ def run(chat, args: str):
218
+ args = args.strip()
219
+
220
+ strip_only = "--strip" in args
221
+ if strip_only:
222
+ args = args.replace("--strip", "").strip()
223
+
224
+ mode = "hybrid"
225
+ m = _re.search(r'--mode\s+(\S+)', args)
226
+ if m:
227
+ mode = m.group(1)
228
+ args = (args[:m.start()] + args[m.end():]).strip()
229
+
230
+ if mode not in ("rules", "list", "inline", "hybrid"):
231
+ print(f"[compress] unknown mode '{mode}'. Use: rules, list, inline, hybrid"); return
232
+
233
+ if args.startswith("file:"):
234
+ fpath = args[5:].strip()
235
+ if not _os.path.isabs(fpath):
236
+ fpath = _os.path.join(_os.getcwd(), fpath)
237
+ try:
238
+ text = open(fpath, encoding="utf-8").read().strip()
239
+ except OSError as e:
240
+ print(f"[compress] {e}"); return
241
+ elif args == "$":
242
+ text = getattr(chat, "_last_output", "").strip()
243
+ if not text:
244
+ print("[compress] no last output"); return
245
+ else:
246
+ text = args
247
+
248
+ if not text:
249
+ print("usage: /flow compress [--mode rules|model|hybrid] [--strip] <text | $ | file: path>")
250
+ return
251
+
252
+ print(f"[compress] input : {text}")
253
+ print(f"[compress] mode : {mode}\n")
254
+
255
+ all_pairs = []
256
+ annotated = text
257
+ final_compressed = None
258
+
259
+ if mode == "rules":
260
+ annotated, all_pairs = _apply_rules(text)
261
+
262
+ elif mode == "list":
263
+ annotated, all_pairs = _model_annotate_list(chat, text)
264
+
265
+ elif mode == "inline":
266
+ annotated, all_pairs = _model_annotate_inline(chat, text)
267
+
268
+ elif mode == "hybrid":
269
+ # step 1: rules on original text
270
+ annotated, rule_pairs = _apply_rules(text)
271
+ all_pairs.extend(rule_pairs)
272
+ if rule_pairs:
273
+ print(f"[compress] rules : {len(rule_pairs)} match(es)")
274
+ # step 2: list-model on the clean (compressed) version of what rules left
275
+ clean_after_rules = _compress(annotated)
276
+ if clean_after_rules != _compress(text): # rules found something
277
+ model_input = clean_after_rules
278
+ else:
279
+ model_input = text
280
+ model_annotated, model_pairs = _model_annotate_list(chat, model_input)
281
+ model_pairs = [(p, r) for p, r in model_pairs if p not in {x[0] for x in rule_pairs}]
282
+ all_pairs = rule_pairs + model_pairs
283
+ # display: rules annotation on original text
284
+ annotated, _ = _apply_rules(text)
285
+ # compressed: sequential — rules clean first, then model strips on top
286
+ seq = clean_after_rules
287
+ for phrase, _ in model_pairs:
288
+ seq = _re.sub(_re.escape(phrase), '', seq, count=1, flags=_re.IGNORECASE)
289
+ seq = _re.sub(r'^[.,;:\-–—\s]+', '', _re.sub(r' +', ' ', seq)).strip()
290
+ final_compressed = seq
291
+
292
+ if not all_pairs:
293
+ print("[compress] no redundancies found")
294
+ compressed = text
295
+ else:
296
+ print(f"[compress] found : {len(all_pairs)} redundanc{'y' if len(all_pairs)==1 else 'ies'}")
297
+ for phrase, reason in all_pairs:
298
+ print(f" '{phrase}' → {reason}")
299
+
300
+ compressed = final_compressed if final_compressed is not None else _compress(annotated)
301
+ ratio = round((1 - len(compressed) / max(len(text), 1)) * 100)
302
+
303
+ if not strip_only:
304
+ print(f"\n[compress] annotated:")
305
+ print(f" {annotated}")
306
+
307
+ print(f"\n[compress] compressed (-{ratio}%):")
308
+ print(f" {compressed}")
309
+
310
+ chat._last_output = compressed