conjira-cli 0.2.1__tar.gz → 0.2.3__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 (31) hide show
  1. {conjira_cli-0.2.1/src/conjira_cli.egg-info → conjira_cli-0.2.3}/PKG-INFO +66 -7
  2. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/README.md +65 -6
  3. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/pyproject.toml +1 -1
  4. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/__init__.py +1 -1
  5. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/cli.py +112 -5
  6. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/client.py +37 -2
  7. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/config.py +13 -2
  8. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/markdown_export.py +101 -2
  9. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/markdown_import.py +66 -0
  10. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/setup_macos.py +12 -1
  11. {conjira_cli-0.2.1 → conjira_cli-0.2.3/src/conjira_cli.egg-info}/PKG-INFO +66 -7
  12. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_cli.py +283 -0
  13. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_client.py +42 -0
  14. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_config.py +56 -1
  15. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_markdown_export.py +101 -0
  16. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_markdown_import.py +61 -0
  17. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_setup_macos.py +15 -0
  18. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/LICENSE +0 -0
  19. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/setup.cfg +0 -0
  20. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/setup.py +0 -0
  21. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/__main__.py +0 -0
  22. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/inline_comments.py +0 -0
  23. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/section_edit.py +0 -0
  24. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/tree_export.py +0 -0
  25. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli.egg-info/SOURCES.txt +0 -0
  26. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli.egg-info/dependency_links.txt +0 -0
  27. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli.egg-info/entry_points.txt +0 -0
  28. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli.egg-info/top_level.txt +0 -0
  29. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_inline_comments.py +0 -0
  30. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_section_edit.py +0 -0
  31. {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_tree_export.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conjira-cli
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Unofficial agent-friendly CLI for self-hosted Confluence and Jira
5
5
  Author: quanttraderkim
6
6
  License-Expression: MIT
@@ -47,6 +47,8 @@ If your team uses self-hosted Confluence and Jira, official cloud-native connect
47
47
 
48
48
  `conjira-cli` is built for that gap. It helps when you want to read Confluence pages, export them to Markdown, refresh stale exports from the live wiki, summarize inline comment threads, search Jira with JQL, or create and update content without hardcoding PATs into source files or chat transcripts.
49
49
 
50
+ For report-style pages, it can also preserve a small set of Confluence-native presentation macros while still keeping Markdown as the source of truth. The current bridge covers Mermaid, Markdown callouts such as `> [!INFO]`, `> [!NOTE]`, `> [!TIP]`, and `> [!WARNING]`, expandable blocks with `> [!EXPAND]`, and inline status badges with `:status[In Progress]{color=yellow}`.
51
+
50
52
  ## What you can do with it
51
53
 
52
54
  - read Confluence pages and search with CQL
@@ -112,6 +114,7 @@ The script stores PATs in macOS Keychain, writes only non-secret settings to `lo
112
114
  It uses the default Keychain target names automatically, so most users only need to enter the base URL and PAT.
113
115
  PAT prompts are hidden on screen by design. Paste the token and press Enter even if nothing appears while typing.
114
116
  It does not write PAT values to `~/.zshrc` or other shell profile files.
117
+ If you keep working in the same folder, `conjira` will auto-load `./local/agent.env` so you do not need to pass `--env-file` each time.
115
118
  If you are running directly from a source checkout before installing entrypoints, you can still use:
116
119
 
117
120
  ```bash
@@ -130,9 +133,55 @@ If you are using Codex, Claude Code, or another shell-capable local coding agent
130
133
  If you want to run the CLI directly, start with these short commands:
131
134
 
132
135
  ```bash
133
- conjira --env-file ./local/agent.env auth-check
134
- conjira --env-file ./local/agent.env jira-auth-check
135
- conjira --env-file ./local/agent.env export-page-md --page-id 123456 --output-dir "/path/to/notes"
136
+ conjira auth-check
137
+ conjira jira-auth-check
138
+ conjira export-page-md --page-id 123456 --output-dir "/path/to/notes"
139
+ ```
140
+
141
+ If you run the CLI from a different folder, pass the config file explicitly with `--env-file /path/to/local/agent.env`.
142
+
143
+ ## Prompt templates by document type
144
+
145
+ When you ask an agent to upload or update a document, results are better if you tell it what kind of document it is, whether Markdown should remain the source of truth, and whether the Confluence page should stay plain or become more presentation-friendly.
146
+
147
+ For skill specs or evaluation docs, a good request is:
148
+
149
+ ```text
150
+ Use conjira to upload this Markdown file as a Confluence page. Treat it as a skill spec, keep the Markdown structure as the source of truth, preserve headings and tables, and only use Confluence-native rendering where it helps readability without changing the document's meaning.
151
+ ```
152
+
153
+ For service planning docs or PRDs, a good request is:
154
+
155
+ ```text
156
+ Use conjira to turn this Markdown file into a Confluence PRD. Keep the content faithful to the source, but make it easier to read in Confluence. Add status, callouts, and expand blocks where they improve readability, and organize the page around summary, background, problem, scope, flow, risks, and open questions.
157
+ ```
158
+
159
+ For strategy reports or review decks, a good request is:
160
+
161
+ ```text
162
+ Use conjira to publish this Markdown file as a report-style Confluence page. Keep the source content intact, but optimize the page for presentation. Put the executive summary first, surface key decisions and risks early, and use status, info blocks, expand sections, and Mermaid where appropriate.
163
+ ```
164
+
165
+ If you want to stay closer to raw Markdown, say `keep this Markdown-first and avoid extra presentation macros`. If you want a more polished Confluence page, say `optimize this for Confluence readability while keeping the source content intact`.
166
+
167
+ ## Document writing style guide
168
+
169
+ For Confluence uploads, structure matters as much as accuracy. Strategy memos, PRDs, planning docs, and report-style pages read much better when the writer uses a compact working-document style instead of long essay-like prose.
170
+
171
+ Prefer report-style memo wording over verbose formal phrasing. In Korean this usually means reducing repetitive `~입니다` and `~합니다`, and favoring shorter noun-phrase or judgment-oriented endings such as `~필요`, `~전제`, `~검토`, `~제안`, or `~우선`. Do not force every sentence into rigid `~함` wording; the target is a concise internal memo tone, not mechanical shorthand. Keep paragraphs short, lead with the conclusion, and make `###` headings carry the message or judgment instead of acting as generic labels.
172
+
173
+ Use bullet points only for true parallel items. Do not split a single thought into fake bullets just to reduce line length. Use tables for comparisons, summaries, options, risks, target groups, ownership, or decision support. Do not force short explanatory text into a table when a compact paragraph is clearer.
174
+
175
+ A good default order is `purpose -> summary -> key judgments / evidence -> risks / assumptions -> implications / next actions`. Strategy memos usually need `why this matters`, `key judgment`, `evidence`, and `implications`. PRDs usually need `problem`, `goal`, `scope`, `flow`, `policy`, and `open issues`. Skill specs are better when they stay literal and dry, with sections like `one-line definition`, `when to use`, `inputs`, `outputs`, `exceptions`, and `evaluation criteria`.
176
+
177
+ For longer strategy or report documents, explicit numbering helps. Top-level sections such as `A.`, `B.`, `C.` and second-level sections such as `A-1.`, `A-2.` usually make the table of contents and the body much easier to scan. As a default, stay within two levels unless the user explicitly wants a deeper document tree.
178
+
179
+ Cut abstract filler. Avoid phrases like “strategically meaningful”, “fundamentally important value”, or “meaningful impact across multiple dimensions” unless they add something precise. Prefer concrete statements such as “follow-up gaps turn into revenue loss”, “A is the right first rollout option”, or “free-user monetization is necessary but total revenue impact is limited”.
180
+
181
+ If you want an agent to follow this style explicitly, a good request is:
182
+
183
+ ```text
184
+ Use conjira to publish this document to Confluence. Keep the source content intact, but rewrite it in a concise internal memo style. Reduce formal `~입니다/~합니다` phrasing, prefer short noun-phrase or judgment-oriented wording, put the key summary first, use stronger h3/h4 headings and tables where they help scanability, add `A.` / `A-1.` numbering when the document is long enough to benefit from it, keep bullets for true parallel items only, and remove abstract filler.
136
185
  ```
137
186
 
138
187
  Short sample output blocks, using synthetic values:
@@ -237,10 +286,12 @@ JIRA_PAT_FILE=/path/to/jira.token
237
286
  Then verify the connection:
238
287
 
239
288
  ```bash
240
- conjira --env-file ./local/agent.env auth-check
241
- conjira --env-file ./local/agent.env jira-auth-check
289
+ conjira auth-check
290
+ conjira jira-auth-check
242
291
  ```
243
292
 
293
+ If you run the CLI outside the configured folder, use `--env-file /path/to/local/agent.env` explicitly.
294
+
244
295
  ## Common commands
245
296
 
246
297
  Read a Confluence page:
@@ -308,6 +359,14 @@ conjira --env-file ./local/agent.env jira-search --jql 'project = DEMO ORDER BY
308
359
  conjira --env-file ./local/agent.env jira-get-issue --issue-key DEMO-123
309
360
  ```
310
361
 
362
+ Inspect updated timestamps or recent comments when needed:
363
+
364
+ ```bash
365
+ conjira jira-get-issue --issue-key DEMO-123 --include-comments --comments-limit 2
366
+ conjira jira-get-issue --issue-key DEMO-123 --raw --fields summary,updated,comment
367
+ conjira jira-search --jql 'project = DEMO ORDER BY updated DESC' --raw --fields summary,updated
368
+ ```
369
+
311
370
  Create a Jira issue or add a comment:
312
371
 
313
372
  ```bash
@@ -374,7 +433,7 @@ The recommended pattern is to set `CONFLUENCE_EXPORT_DEFAULT_DIR` to an inbox or
374
433
 
375
434
  ## Markdown import notes
376
435
 
377
- Markdown upload is a best-effort conversion to Confluence storage HTML. It works well for common headings, paragraphs, lists, blockquotes, fenced code blocks, tables, links, images, and simple wiki-style links. It is not a perfect round-trip for complex Confluence macros, merged tables, or deeply nested layouts, so treat Markdown import as a practical authoring path rather than a lossless document converter.
436
+ Markdown upload is a best-effort conversion to Confluence storage HTML. It works well for common headings, paragraphs, lists, blockquotes, fenced code blocks, tables, links, images, simple wiki-style links, and the currently supported report macros (`mermaid`, callouts, `expand`, and `:status[Title]{color=blue}`). It is not a perfect round-trip for complex Confluence macros, merged tables, or deeply nested layouts, so treat Markdown import as a practical authoring path rather than a lossless document converter.
378
437
 
379
438
  Use `--body-file` and `--append-file` only for storage HTML files. If your source file is Markdown, use `--body-markdown-file` or `--append-markdown-file` so the CLI converts it before upload.
380
439
 
@@ -18,6 +18,8 @@ If your team uses self-hosted Confluence and Jira, official cloud-native connect
18
18
 
19
19
  `conjira-cli` is built for that gap. It helps when you want to read Confluence pages, export them to Markdown, refresh stale exports from the live wiki, summarize inline comment threads, search Jira with JQL, or create and update content without hardcoding PATs into source files or chat transcripts.
20
20
 
21
+ For report-style pages, it can also preserve a small set of Confluence-native presentation macros while still keeping Markdown as the source of truth. The current bridge covers Mermaid, Markdown callouts such as `> [!INFO]`, `> [!NOTE]`, `> [!TIP]`, and `> [!WARNING]`, expandable blocks with `> [!EXPAND]`, and inline status badges with `:status[In Progress]{color=yellow}`.
22
+
21
23
  ## What you can do with it
22
24
 
23
25
  - read Confluence pages and search with CQL
@@ -83,6 +85,7 @@ The script stores PATs in macOS Keychain, writes only non-secret settings to `lo
83
85
  It uses the default Keychain target names automatically, so most users only need to enter the base URL and PAT.
84
86
  PAT prompts are hidden on screen by design. Paste the token and press Enter even if nothing appears while typing.
85
87
  It does not write PAT values to `~/.zshrc` or other shell profile files.
88
+ If you keep working in the same folder, `conjira` will auto-load `./local/agent.env` so you do not need to pass `--env-file` each time.
86
89
  If you are running directly from a source checkout before installing entrypoints, you can still use:
87
90
 
88
91
  ```bash
@@ -101,9 +104,55 @@ If you are using Codex, Claude Code, or another shell-capable local coding agent
101
104
  If you want to run the CLI directly, start with these short commands:
102
105
 
103
106
  ```bash
104
- conjira --env-file ./local/agent.env auth-check
105
- conjira --env-file ./local/agent.env jira-auth-check
106
- conjira --env-file ./local/agent.env export-page-md --page-id 123456 --output-dir "/path/to/notes"
107
+ conjira auth-check
108
+ conjira jira-auth-check
109
+ conjira export-page-md --page-id 123456 --output-dir "/path/to/notes"
110
+ ```
111
+
112
+ If you run the CLI from a different folder, pass the config file explicitly with `--env-file /path/to/local/agent.env`.
113
+
114
+ ## Prompt templates by document type
115
+
116
+ When you ask an agent to upload or update a document, results are better if you tell it what kind of document it is, whether Markdown should remain the source of truth, and whether the Confluence page should stay plain or become more presentation-friendly.
117
+
118
+ For skill specs or evaluation docs, a good request is:
119
+
120
+ ```text
121
+ Use conjira to upload this Markdown file as a Confluence page. Treat it as a skill spec, keep the Markdown structure as the source of truth, preserve headings and tables, and only use Confluence-native rendering where it helps readability without changing the document's meaning.
122
+ ```
123
+
124
+ For service planning docs or PRDs, a good request is:
125
+
126
+ ```text
127
+ Use conjira to turn this Markdown file into a Confluence PRD. Keep the content faithful to the source, but make it easier to read in Confluence. Add status, callouts, and expand blocks where they improve readability, and organize the page around summary, background, problem, scope, flow, risks, and open questions.
128
+ ```
129
+
130
+ For strategy reports or review decks, a good request is:
131
+
132
+ ```text
133
+ Use conjira to publish this Markdown file as a report-style Confluence page. Keep the source content intact, but optimize the page for presentation. Put the executive summary first, surface key decisions and risks early, and use status, info blocks, expand sections, and Mermaid where appropriate.
134
+ ```
135
+
136
+ If you want to stay closer to raw Markdown, say `keep this Markdown-first and avoid extra presentation macros`. If you want a more polished Confluence page, say `optimize this for Confluence readability while keeping the source content intact`.
137
+
138
+ ## Document writing style guide
139
+
140
+ For Confluence uploads, structure matters as much as accuracy. Strategy memos, PRDs, planning docs, and report-style pages read much better when the writer uses a compact working-document style instead of long essay-like prose.
141
+
142
+ Prefer report-style memo wording over verbose formal phrasing. In Korean this usually means reducing repetitive `~입니다` and `~합니다`, and favoring shorter noun-phrase or judgment-oriented endings such as `~필요`, `~전제`, `~검토`, `~제안`, or `~우선`. Do not force every sentence into rigid `~함` wording; the target is a concise internal memo tone, not mechanical shorthand. Keep paragraphs short, lead with the conclusion, and make `###` headings carry the message or judgment instead of acting as generic labels.
143
+
144
+ Use bullet points only for true parallel items. Do not split a single thought into fake bullets just to reduce line length. Use tables for comparisons, summaries, options, risks, target groups, ownership, or decision support. Do not force short explanatory text into a table when a compact paragraph is clearer.
145
+
146
+ A good default order is `purpose -> summary -> key judgments / evidence -> risks / assumptions -> implications / next actions`. Strategy memos usually need `why this matters`, `key judgment`, `evidence`, and `implications`. PRDs usually need `problem`, `goal`, `scope`, `flow`, `policy`, and `open issues`. Skill specs are better when they stay literal and dry, with sections like `one-line definition`, `when to use`, `inputs`, `outputs`, `exceptions`, and `evaluation criteria`.
147
+
148
+ For longer strategy or report documents, explicit numbering helps. Top-level sections such as `A.`, `B.`, `C.` and second-level sections such as `A-1.`, `A-2.` usually make the table of contents and the body much easier to scan. As a default, stay within two levels unless the user explicitly wants a deeper document tree.
149
+
150
+ Cut abstract filler. Avoid phrases like “strategically meaningful”, “fundamentally important value”, or “meaningful impact across multiple dimensions” unless they add something precise. Prefer concrete statements such as “follow-up gaps turn into revenue loss”, “A is the right first rollout option”, or “free-user monetization is necessary but total revenue impact is limited”.
151
+
152
+ If you want an agent to follow this style explicitly, a good request is:
153
+
154
+ ```text
155
+ Use conjira to publish this document to Confluence. Keep the source content intact, but rewrite it in a concise internal memo style. Reduce formal `~입니다/~합니다` phrasing, prefer short noun-phrase or judgment-oriented wording, put the key summary first, use stronger h3/h4 headings and tables where they help scanability, add `A.` / `A-1.` numbering when the document is long enough to benefit from it, keep bullets for true parallel items only, and remove abstract filler.
107
156
  ```
108
157
 
109
158
  Short sample output blocks, using synthetic values:
@@ -208,10 +257,12 @@ JIRA_PAT_FILE=/path/to/jira.token
208
257
  Then verify the connection:
209
258
 
210
259
  ```bash
211
- conjira --env-file ./local/agent.env auth-check
212
- conjira --env-file ./local/agent.env jira-auth-check
260
+ conjira auth-check
261
+ conjira jira-auth-check
213
262
  ```
214
263
 
264
+ If you run the CLI outside the configured folder, use `--env-file /path/to/local/agent.env` explicitly.
265
+
215
266
  ## Common commands
216
267
 
217
268
  Read a Confluence page:
@@ -279,6 +330,14 @@ conjira --env-file ./local/agent.env jira-search --jql 'project = DEMO ORDER BY
279
330
  conjira --env-file ./local/agent.env jira-get-issue --issue-key DEMO-123
280
331
  ```
281
332
 
333
+ Inspect updated timestamps or recent comments when needed:
334
+
335
+ ```bash
336
+ conjira jira-get-issue --issue-key DEMO-123 --include-comments --comments-limit 2
337
+ conjira jira-get-issue --issue-key DEMO-123 --raw --fields summary,updated,comment
338
+ conjira jira-search --jql 'project = DEMO ORDER BY updated DESC' --raw --fields summary,updated
339
+ ```
340
+
282
341
  Create a Jira issue or add a comment:
283
342
 
284
343
  ```bash
@@ -345,7 +404,7 @@ The recommended pattern is to set `CONFLUENCE_EXPORT_DEFAULT_DIR` to an inbox or
345
404
 
346
405
  ## Markdown import notes
347
406
 
348
- Markdown upload is a best-effort conversion to Confluence storage HTML. It works well for common headings, paragraphs, lists, blockquotes, fenced code blocks, tables, links, images, and simple wiki-style links. It is not a perfect round-trip for complex Confluence macros, merged tables, or deeply nested layouts, so treat Markdown import as a practical authoring path rather than a lossless document converter.
407
+ Markdown upload is a best-effort conversion to Confluence storage HTML. It works well for common headings, paragraphs, lists, blockquotes, fenced code blocks, tables, links, images, simple wiki-style links, and the currently supported report macros (`mermaid`, callouts, `expand`, and `:status[Title]{color=blue}`). It is not a perfect round-trip for complex Confluence macros, merged tables, or deeply nested layouts, so treat Markdown import as a practical authoring path rather than a lossless document converter.
349
408
 
350
409
  Use `--body-file` and `--append-file` only for storage HTML files. If your source file is Markdown, use `--body-markdown-file` or `--append-markdown-file` so the CLI converts it before upload.
351
410
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "conjira-cli"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  description = "Unofficial agent-friendly CLI for self-hosted Confluence and Jira"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.3"
@@ -26,6 +26,16 @@ from conjira_cli.markdown_import import markdown_to_storage_html
26
26
  from conjira_cli.section_edit import SectionEditError, replace_section_html
27
27
  from conjira_cli.tree_export import export_page_tree, sanitize_path_component
28
28
 
29
+ _JIRA_SUMMARY_FIELDS = [
30
+ "summary",
31
+ "status",
32
+ "issuetype",
33
+ "project",
34
+ "assignee",
35
+ "reporter",
36
+ "updated",
37
+ ]
38
+
29
39
 
30
40
  def _read_text_arg(raw_text: Optional[str], file_path: Optional[str]) -> str:
31
41
  if raw_text is not None:
@@ -43,6 +53,21 @@ def _read_json_arg(raw_json: Optional[str], file_path: Optional[str]) -> Dict[st
43
53
  return {}
44
54
 
45
55
 
56
+ def _merge_csv_fields(raw_fields: Optional[str], required_fields: list[str]) -> Optional[str]:
57
+ tokens: list[str] = []
58
+ seen: set[str] = set()
59
+ for value in [raw_fields, ",".join(required_fields)]:
60
+ if not value:
61
+ continue
62
+ for token in value.split(","):
63
+ item = token.strip()
64
+ if not item or item in seen:
65
+ continue
66
+ seen.add(item)
67
+ tokens.append(item)
68
+ return ",".join(tokens) if tokens else None
69
+
70
+
46
71
  def _read_confluence_body_arg(
47
72
  raw_html: Optional[str],
48
73
  html_file: Optional[str],
@@ -104,6 +129,62 @@ def _preview_html(value: Optional[str], limit: int = 240) -> Optional[str]:
104
129
  return _preview_text(preview, limit=limit)
105
130
 
106
131
 
132
+ def _page_body_html(page: Dict[str, Any]) -> str:
133
+ return (((page.get("body") or {}).get("storage") or {}).get("value")) or ""
134
+
135
+
136
+ def _is_effectively_empty_body(body_html: Optional[str]) -> bool:
137
+ if not body_html:
138
+ return True
139
+ normalized = body_html.replace("\xa0", " ").replace(" ", " ")
140
+ normalized = re.sub(r"<!--.*?-->", "", normalized, flags=re.DOTALL)
141
+ normalized = re.sub(r"<p>\s*(<br\s*/?>)?\s*</p>", "", normalized, flags=re.IGNORECASE)
142
+ normalized = re.sub(r"<br\s*/?>", "", normalized, flags=re.IGNORECASE)
143
+ normalized = re.sub(r"\s+", "", normalized)
144
+ return normalized == ""
145
+
146
+
147
+ def _summarize_child_pages(child_pages: list[Dict[str, Any]]) -> list[Dict[str, Any]]:
148
+ return [ConfluenceClient.summarize_page(page) for page in child_pages]
149
+
150
+
151
+ def _fallback_confluence_page_url(page: Dict[str, Any], page_id: Optional[str]) -> Optional[str]:
152
+ if not page_id:
153
+ return None
154
+ base_url = ((page.get("_links") or {}).get("base")) or ""
155
+ if not base_url:
156
+ return None
157
+ return "{0}/pages/viewpage.action?pageId={1}".format(base_url.rstrip("/"), page_id)
158
+
159
+
160
+ def _page_navigation_payload(
161
+ *,
162
+ page: Dict[str, Any],
163
+ child_pages: list[Dict[str, Any]],
164
+ ) -> Dict[str, Any]:
165
+ body_html = _page_body_html(page)
166
+ body_is_effectively_empty = _is_effectively_empty_body(body_html)
167
+ child_summaries = _summarize_child_pages(child_pages)
168
+ for summary in child_summaries:
169
+ if not summary.get("webui_url"):
170
+ summary["webui_url"] = _fallback_confluence_page_url(page, summary.get("id"))
171
+ page_kind = "hub" if body_is_effectively_empty and child_summaries else "content"
172
+
173
+ payload: Dict[str, Any] = {
174
+ "page_kind": page_kind,
175
+ "body_is_effectively_empty": body_is_effectively_empty,
176
+ "child_count": len(child_summaries),
177
+ }
178
+ if child_summaries:
179
+ payload["children"] = child_summaries
180
+ if page_kind == "hub":
181
+ payload["read_hint"] = (
182
+ "This page behaves like a hub/index page. Read the listed child pages or use "
183
+ "`export-tree-md` for the full hierarchy."
184
+ )
185
+ return payload
186
+
187
+
107
188
  def _sanitize_markdown_filename(title: str) -> str:
108
189
  sanitized = "".join(
109
190
  "_" if char in '<>:"/\\|?*' else char
@@ -156,7 +237,7 @@ def _read_export_metadata(path: Path) -> Dict[str, Any]:
156
237
 
157
238
  def _page_export_payload(page: Dict[str, Any]) -> Dict[str, Any]:
158
239
  payload = ConfluenceClient.summarize_page(page)
159
- payload["body_html"] = (((page.get("body") or {}).get("storage") or {}).get("value")) or ""
240
+ payload["body_html"] = _page_body_html(page)
160
241
  payload["ancestors"] = page.get("ancestors") or []
161
242
  return payload
162
243
 
@@ -353,6 +434,9 @@ def _build_parser() -> argparse.ArgumentParser:
353
434
  jira_get_issue.add_argument("--issue-key", required=True)
354
435
  jira_get_issue.add_argument("--fields")
355
436
  jira_get_issue.add_argument("--expand")
437
+ jira_get_issue.add_argument("--include-comments", action="store_true")
438
+ jira_get_issue.add_argument("--comments-limit", type=int, default=3)
439
+ jira_get_issue.add_argument("--raw", action="store_true")
356
440
 
357
441
  jira_search = subparsers.add_parser("jira-search", help="Search Jira with JQL")
358
442
  jira_search.add_argument("--jql", required=True)
@@ -360,6 +444,7 @@ def _build_parser() -> argparse.ArgumentParser:
360
444
  jira_search.add_argument("--start", type=int, default=0)
361
445
  jira_search.add_argument("--fields")
362
446
  jira_search.add_argument("--expand")
447
+ jira_search.add_argument("--raw", action="store_true")
363
448
 
364
449
  jira_get_createmeta = subparsers.add_parser(
365
450
  "jira-get-createmeta",
@@ -793,14 +878,19 @@ def _handle_confluence(args: argparse.Namespace) -> Dict[str, Any]:
793
878
  if args.command == "auth-check":
794
879
  return client.auth_check()
795
880
  if args.command == "get-page":
796
- page = client.get_page(args.page_id, expand=args.expand)
881
+ expand = _merge_csv_fields(args.expand, ["body.storage", "version", "space"])
882
+ page = client.get_page(args.page_id, expand=expand)
883
+ child_pages = client.list_child_pages(args.page_id)
797
884
  payload = client.summarize_page(page)
885
+ payload.update(_page_navigation_payload(page=page, child_pages=child_pages))
798
886
  if args.expand and "body.storage" in args.expand:
799
- payload["body_html"] = (((page.get("body") or {}).get("storage") or {}).get("value"))
887
+ payload["body_html"] = _page_body_html(page)
800
888
  return payload
801
889
  if args.command == "export-page-md":
802
890
  page = client.get_page(args.page_id, expand="body.storage,version,space")
891
+ child_pages = client.list_child_pages(args.page_id)
803
892
  payload = _page_export_payload(page)
893
+ payload.update(_page_navigation_payload(page=page, child_pages=child_pages))
804
894
  exporter = MarkdownExporter(
805
895
  base_url=settings.base_url,
806
896
  page_id=args.page_id,
@@ -823,6 +913,9 @@ def _handle_confluence(args: argparse.Namespace) -> Dict[str, Any]:
823
913
  "title": payload["title"],
824
914
  "output_file": str(output_path),
825
915
  "source_url": payload["webui_url"],
916
+ "page_kind": payload["page_kind"],
917
+ "child_count": payload["child_count"],
918
+ "hub_generated": payload["page_kind"] == "hub",
826
919
  "used_staging_local": str(output_path).startswith(
827
920
  str((Path(settings.export_staging_dir) if settings.export_staging_dir else _default_export_staging_dir()))
828
921
  ),
@@ -1193,8 +1286,20 @@ def _handle_jira(args: argparse.Namespace) -> Dict[str, Any]:
1193
1286
  if args.command == "jira-auth-check":
1194
1287
  return client.auth_check()
1195
1288
  if args.command == "jira-get-issue":
1196
- issue = client.get_issue(args.issue_key, fields=args.fields, expand=args.expand)
1197
- return client.summarize_issue(issue)
1289
+ fields = args.fields
1290
+ if args.include_comments:
1291
+ required_fields = ["comment", "updated"]
1292
+ if args.fields is None:
1293
+ required_fields = _JIRA_SUMMARY_FIELDS + ["comment"]
1294
+ fields = _merge_csv_fields(args.fields, required_fields)
1295
+ issue = client.get_issue(args.issue_key, fields=fields, expand=args.expand)
1296
+ if args.raw:
1297
+ return issue
1298
+ return client.summarize_issue(
1299
+ issue,
1300
+ include_comments=args.include_comments,
1301
+ comments_limit=args.comments_limit,
1302
+ )
1198
1303
  if args.command == "jira-search":
1199
1304
  result = client.search(
1200
1305
  jql=args.jql,
@@ -1203,6 +1308,8 @@ def _handle_jira(args: argparse.Namespace) -> Dict[str, Any]:
1203
1308
  fields=args.fields,
1204
1309
  expand=args.expand,
1205
1310
  )
1311
+ if args.raw:
1312
+ return result
1206
1313
  return client.summarize_search_results(result.get("issues", []))
1207
1314
  if args.command == "jira-get-createmeta":
1208
1315
  result = client.get_createmeta(
@@ -540,14 +540,32 @@ class JiraClient(BaseAtlassianClient):
540
540
  return None
541
541
  return "{0}/browse/{1}".format(base_url.rstrip("/"), issue_key)
542
542
 
543
- def summarize_issue(self, issue: Dict[str, Any]) -> Dict[str, Any]:
543
+ @staticmethod
544
+ def _comment_body_preview(value: Any, limit: int = 280) -> Optional[str]:
545
+ if value is None:
546
+ return None
547
+ if isinstance(value, str):
548
+ collapsed = " ".join(value.split())
549
+ else:
550
+ collapsed = " ".join(json.dumps(value, ensure_ascii=False).split())
551
+ if len(collapsed) <= limit:
552
+ return collapsed
553
+ return collapsed[: limit - 1].rstrip() + "…"
554
+
555
+ def summarize_issue(
556
+ self,
557
+ issue: Dict[str, Any],
558
+ *,
559
+ include_comments: bool = False,
560
+ comments_limit: int = 3,
561
+ ) -> Dict[str, Any]:
544
562
  fields = issue.get("fields") or {}
545
563
  status = fields.get("status") or {}
546
564
  issue_type = fields.get("issuetype") or {}
547
565
  project = fields.get("project") or {}
548
566
  assignee = fields.get("assignee") or {}
549
567
  reporter = fields.get("reporter") or {}
550
- return {
568
+ summary = {
551
569
  "id": issue.get("id"),
552
570
  "key": issue.get("key"),
553
571
  "summary": fields.get("summary"),
@@ -556,8 +574,25 @@ class JiraClient(BaseAtlassianClient):
556
574
  "project_key": project.get("key"),
557
575
  "assignee": assignee.get("displayName"),
558
576
  "reporter": reporter.get("displayName"),
577
+ "updated": fields.get("updated"),
559
578
  "browse_url": self.browse_url(self.base_url, issue.get("key")),
560
579
  }
580
+ if include_comments:
581
+ comment_block = fields.get("comment") or {}
582
+ comments = comment_block.get("comments") or []
583
+ limited = comments[-comments_limit:] if comments_limit > 0 else comments
584
+ summary["comment_count"] = comment_block.get("total", len(comments))
585
+ summary["recent_comments"] = [
586
+ {
587
+ "id": comment.get("id"),
588
+ "author": ((comment.get("author") or {}).get("displayName")),
589
+ "created": comment.get("created"),
590
+ "updated": comment.get("updated"),
591
+ "body_preview": self._comment_body_preview(comment.get("body")),
592
+ }
593
+ for comment in limited
594
+ ]
595
+ return summary
561
596
 
562
597
  def summarize_search_results(self, items: Iterable[Dict[str, Any]]) -> Dict[str, Any]:
563
598
  results = [self.summarize_issue(item) for item in items]
@@ -56,6 +56,16 @@ def load_env_file(path: Path) -> Dict[str, str]:
56
56
  return values
57
57
 
58
58
 
59
+ def resolve_env_file_path(env_file: Optional[str]) -> Optional[str]:
60
+ if env_file:
61
+ return str(Path(env_file).expanduser())
62
+
63
+ default_local_env = Path.cwd() / "local" / "agent.env"
64
+ if default_local_env.exists():
65
+ return str(default_local_env)
66
+ return None
67
+
68
+
59
69
  def _parse_csv_set(value: Optional[str]) -> Optional[Set[str]]:
60
70
  if not value:
61
71
  return None
@@ -103,8 +113,9 @@ def _resolve_common_settings(
103
113
  env_file: Optional[str] = None,
104
114
  ) -> Tuple[Dict[str, str], str, str, int]:
105
115
  env: Dict[str, str] = {}
106
- if env_file:
107
- env = load_env_file(Path(env_file))
116
+ resolved_env_file = resolve_env_file_path(env_file)
117
+ if resolved_env_file:
118
+ env = load_env_file(Path(resolved_env_file))
108
119
 
109
120
  base_url_key = "{0}_BASE_URL".format(prefix)
110
121
  token_key = "{0}_PAT".format(prefix)