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.
- {conjira_cli-0.2.1/src/conjira_cli.egg-info → conjira_cli-0.2.3}/PKG-INFO +66 -7
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/README.md +65 -6
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/pyproject.toml +1 -1
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/__init__.py +1 -1
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/cli.py +112 -5
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/client.py +37 -2
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/config.py +13 -2
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/markdown_export.py +101 -2
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/markdown_import.py +66 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/setup_macos.py +12 -1
- {conjira_cli-0.2.1 → conjira_cli-0.2.3/src/conjira_cli.egg-info}/PKG-INFO +66 -7
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_cli.py +283 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_client.py +42 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_config.py +56 -1
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_markdown_export.py +101 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_markdown_import.py +61 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_setup_macos.py +15 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/LICENSE +0 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/setup.cfg +0 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/setup.py +0 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/__main__.py +0 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/inline_comments.py +0 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/section_edit.py +0 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli/tree_export.py +0 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli.egg-info/SOURCES.txt +0 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli.egg-info/dependency_links.txt +0 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli.egg-info/entry_points.txt +0 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/src/conjira_cli.egg-info/top_level.txt +0 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_inline_comments.py +0 -0
- {conjira_cli-0.2.1 → conjira_cli-0.2.3}/tests/test_section_edit.py +0 -0
- {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.
|
|
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
|
|
134
|
-
conjira
|
|
135
|
-
conjira
|
|
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
|
|
241
|
-
conjira
|
|
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,
|
|
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
|
|
105
|
-
conjira
|
|
106
|
-
conjira
|
|
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
|
|
212
|
-
conjira
|
|
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,
|
|
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
|
|
|
@@ -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"] = (
|
|
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
|
-
|
|
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"] = (
|
|
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
|
-
|
|
1197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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)
|