@trendai-crem/claude-skills 0.8.0 → 0.10.0
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.
- package/cli.js +176 -0
- package/marketplace.json +23 -0
- package/package.json +2 -1
- package/skills/atlassian-tools/SKILL.md +0 -361
- package/skills/atlassian-tools/__pycache__/storage_utils.cpython-313.pyc +0 -0
- package/skills/atlassian-tools/confluence_cli.py +0 -796
- package/skills/atlassian-tools/confluence_client/README.md +0 -154
- package/skills/atlassian-tools/confluence_client/__init__.py +0 -44
- package/skills/atlassian-tools/confluence_client/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/atlassian-tools/confluence_client/__pycache__/client.cpython-313.pyc +0 -0
- package/skills/atlassian-tools/confluence_client/__pycache__/commands.cpython-313.pyc +0 -0
- package/skills/atlassian-tools/confluence_client/__pycache__/converter.cpython-313.pyc +0 -0
- package/skills/atlassian-tools/confluence_client/__pycache__/exceptions.cpython-313.pyc +0 -0
- package/skills/atlassian-tools/confluence_client/__pycache__/storage.cpython-313.pyc +0 -0
- package/skills/atlassian-tools/confluence_client/client.py +0 -577
- package/skills/atlassian-tools/confluence_client/commands.py +0 -613
- package/skills/atlassian-tools/confluence_client/converter.py +0 -892
- package/skills/atlassian-tools/confluence_client/exceptions.py +0 -52
- package/skills/atlassian-tools/confluence_client/storage.py +0 -718
- package/skills/atlassian-tools/jira_cli.py +0 -891
- package/skills/atlassian-tools/jira_client/README.md +0 -235
- package/skills/atlassian-tools/jira_client/__init__.py +0 -34
- package/skills/atlassian-tools/jira_client/adf_converter.py +0 -845
- package/skills/atlassian-tools/jira_client/client.py +0 -366
- package/skills/atlassian-tools/jira_client/commands.py +0 -792
- package/skills/atlassian-tools/jira_client/exceptions.py +0 -48
- package/skills/atlassian-tools/jira_client/storage.py +0 -701
- package/skills/atlassian-tools/pyproject.toml +0 -20
- package/skills/atlassian-tools/references/reference.md +0 -473
- package/skills/atlassian-tools/storage_utils.py +0 -29
- package/skills/atlassian-tools/tests/__init__.py +0 -1
- package/skills/atlassian-tools/tests/test_adf_converter.py +0 -1521
- package/skills/atlassian-tools/tests/test_converter.py +0 -1243
- package/skills/atlassian-tools/tests/test_storage_path.py +0 -125
- package/skills/cpp/SKILL.md +0 -350
- package/skills/cpp/references/best-practices.md +0 -299
- package/skills/cpp/references/formatting.md +0 -298
- package/skills/cpp/references/naming.md +0 -280
- package/skills/go/SKILL.md +0 -337
- package/skills/go/references/best-practices.md +0 -262
- package/skills/go/references/formatting.md +0 -247
- package/skills/go/references/naming.md +0 -286
- package/skills/java/SKILL.md +0 -289
- package/skills/java/references/best-practices.md +0 -295
- package/skills/java/references/formatting.md +0 -315
- package/skills/java/references/naming.md +0 -211
- package/skills/javascript/SKILL.md +0 -246
- package/skills/javascript/references/best-practices.md +0 -289
- package/skills/javascript/references/formatting.md +0 -256
- package/skills/javascript/references/naming.md +0 -172
- package/skills/python/SKILL.md +0 -250
- package/skills/python/references/best-practices.md +0 -275
- package/skills/python/references/formatting.md +0 -230
- package/skills/python/references/naming.md +0 -218
- package/skills/shell/SKILL.md +0 -239
- package/skills/shell/references/best-practices.md +0 -256
- package/skills/shell/references/formatting.md +0 -287
- package/skills/shell/references/naming.md +0 -202
- package/skills/typescript/SKILL.md +0 -262
- package/skills/typescript/references/best-practices.md +0 -304
- package/skills/typescript/references/formatting.md +0 -218
- package/skills/typescript/references/naming.md +0 -198
- package/skills/wiki-generation/SKILL.md +0 -121
package/cli.js
CHANGED
|
@@ -38,6 +38,14 @@ const teamSkills = readdirSync(join(__dir, 'skills'), { withFileTypes: true })
|
|
|
38
38
|
|
|
39
39
|
const teamOk = run(['skills', 'add', __dir, '--all', '-g', '-y'], 'team skills');
|
|
40
40
|
|
|
41
|
+
// 3. Marketplace plugins — failures are non-fatal
|
|
42
|
+
let marketplaceResults = [];
|
|
43
|
+
try {
|
|
44
|
+
marketplaceResults = installMarketplacePlugins();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(`\nmarketplace plugin installation failed unexpectedly: ${err.message}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
// Summary
|
|
42
50
|
console.log('\nResults:');
|
|
43
51
|
externalResults.forEach(({ label, ok }) => console.log(` ${ok ? '✓' : '✗'} ${label}`));
|
|
@@ -46,6 +54,9 @@ if (teamOk) {
|
|
|
46
54
|
} else {
|
|
47
55
|
console.log(` ✗ team skills`);
|
|
48
56
|
}
|
|
57
|
+
marketplaceResults.forEach(({ plugin, action, ok }) =>
|
|
58
|
+
console.log(` ${ok ? '✓' : '✗'} ${plugin} (${action})`)
|
|
59
|
+
);
|
|
49
60
|
|
|
50
61
|
if (!teamOk) {
|
|
51
62
|
console.error('\nFATAL: Team skills installation failed.');
|
|
@@ -57,9 +68,174 @@ if (externalFailed.length > 0) {
|
|
|
57
68
|
console.warn(`\nWARN: ${externalFailed.length} external source(s) failed — team skills installed successfully.`);
|
|
58
69
|
}
|
|
59
70
|
|
|
71
|
+
const marketplaceFailed = marketplaceResults.filter(r => !r.ok);
|
|
72
|
+
if (marketplaceFailed.length > 0) {
|
|
73
|
+
console.warn(`\nWARN: ${marketplaceFailed.length} marketplace plugin(s) failed — team skills installed successfully.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
// Configure update-check hooks
|
|
61
77
|
setupAutoUpdate();
|
|
62
78
|
|
|
79
|
+
// Reads a JSON file and returns its parsed value if it is a non-null, non-array object.
|
|
80
|
+
// Returns `fallback` on any error (missing file, parse failure, wrong type).
|
|
81
|
+
function readJsonObject(filePath, fallback, label) {
|
|
82
|
+
if (!existsSync(filePath)) return fallback;
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
85
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
|
86
|
+
console.warn(`\nWARN: Ignoring invalid ${label}: expected JSON object`);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.warn(`\nWARN: Ignoring invalid ${label}: ${err.message}`);
|
|
89
|
+
}
|
|
90
|
+
return fallback;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Extracts the registered source for a marketplace from known_marketplaces.json.
|
|
94
|
+
// Handles: plain string, object with string .source, and GitHub registry format
|
|
95
|
+
// { source: { source: "github", repo: "org/repo" } }.
|
|
96
|
+
function getRegisteredMarketplaceSource(known, marketplaceName) {
|
|
97
|
+
const entry = known[marketplaceName];
|
|
98
|
+
if (typeof entry === 'string') return entry;
|
|
99
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
100
|
+
if (typeof entry.source === 'string') return entry.source;
|
|
101
|
+
// GitHub registry format: { source: { source: "github", repo: "org/repo" }, ... }
|
|
102
|
+
if (entry.source?.source === 'github' && typeof entry.source?.repo === 'string') {
|
|
103
|
+
return entry.source.repo;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Normalizes a marketplace source to a canonical host/path form for comparison.
|
|
110
|
+
// Accepts SSH URLs, HTTPS URLs, and GitHub shorthand (org/repo).
|
|
111
|
+
// Returns null if the source cannot be parsed.
|
|
112
|
+
function normalizeMarketplaceSource(rawSource) {
|
|
113
|
+
// GitHub shorthand: org/repo (no slashes in org, no protocol)
|
|
114
|
+
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(rawSource)) {
|
|
115
|
+
return rawSource.toLowerCase();
|
|
116
|
+
}
|
|
117
|
+
const sshMatch = /^git@([^:]+):(.+?)(?:\.git)?$/.exec(rawSource);
|
|
118
|
+
if (sshMatch) {
|
|
119
|
+
return `${sshMatch[1].toLowerCase()}/${sshMatch[2].replace(/^\/+/, '').replace(/\.git$/, '')}`;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const url = new URL(rawSource);
|
|
123
|
+
return `${url.hostname.toLowerCase()}/${url.pathname.replace(/^\/+/, '').replace(/\.git$/, '')}`;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Installs or updates all plugins for a single marketplace entry.
|
|
130
|
+
// Returns an array of { plugin, action, ok } results.
|
|
131
|
+
function installFromMarketplace(entry, known, installed) {
|
|
132
|
+
// Input validation + allowlisting (SCD-1, OWASP-A04)
|
|
133
|
+
// Leading `-` is rejected to prevent argv injection: `claude plugin install -s` parses as flag.
|
|
134
|
+
const SAFE_NAME = /^(?!-)[A-Za-z0-9_][A-Za-z0-9_-]*$/;
|
|
135
|
+
const SAFE_SOURCE = /^(git@[\w.-]+:[\w.\-/]+\.git|https:\/\/[\w.-]+\/[\w.\-/]+\.git)$/;
|
|
136
|
+
const SAFE_GITHUB = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
137
|
+
|
|
138
|
+
const { name: marketplaceName, source, plugins } = entry ?? {};
|
|
139
|
+
|
|
140
|
+
if (typeof marketplaceName !== 'string' || typeof source !== 'string' || !Array.isArray(plugins)) {
|
|
141
|
+
console.error('\nInvalid marketplace entry: missing required fields (name, source, plugins[])');
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
if (!SAFE_NAME.test(marketplaceName) || (!SAFE_SOURCE.test(source) && !SAFE_GITHUB.test(source))) {
|
|
145
|
+
console.error(`\nInvalid marketplace name or source: ${marketplaceName}`);
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
// Reject the entire entry on any invalid plugin name (fail-closed, not silent filter).
|
|
149
|
+
const invalidPlugins = plugins.filter(p => typeof p !== 'string' || !SAFE_NAME.test(p));
|
|
150
|
+
if (invalidPlugins.length > 0) {
|
|
151
|
+
console.error(`\nInvalid plugin names for ${marketplaceName}: ${invalidPlugins.map(p => JSON.stringify(p)).join(', ')}`);
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(`\nInstalling marketplace plugins (${marketplaceName})...\n`);
|
|
156
|
+
|
|
157
|
+
// Register marketplace if not already known — verify source matches to prevent supply-chain mismatch (OWASP-A08)
|
|
158
|
+
const registeredSource = getRegisteredMarketplaceSource(known, marketplaceName);
|
|
159
|
+
const normalizedSource = normalizeMarketplaceSource(source);
|
|
160
|
+
const normalizedRegistered = registeredSource ? normalizeMarketplaceSource(registeredSource) : null;
|
|
161
|
+
|
|
162
|
+
if (registeredSource && (!normalizedRegistered || normalizedRegistered !== normalizedSource)) {
|
|
163
|
+
console.error(`\nMarketplace source mismatch for ${marketplaceName}: registered as ${registeredSource}, but marketplace.json specifies ${source}`);
|
|
164
|
+
return plugins.map(plugin => ({ plugin, action: 'skipped (source mismatch)', ok: false }));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!registeredSource) {
|
|
168
|
+
try {
|
|
169
|
+
execFileSync('claude', ['plugin', 'marketplace', 'add', source, '--scope', 'user'], {
|
|
170
|
+
stdio: 'inherit',
|
|
171
|
+
});
|
|
172
|
+
} catch (error) {
|
|
173
|
+
const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
|
|
174
|
+
console.error(`\nFailed to register marketplace: ${marketplaceName} (${exitInfo})`);
|
|
175
|
+
return plugins.map(plugin => ({ plugin, action: 'skipped (no marketplace)', ok: false }));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Always update marketplace to pull latest plugin versions before install/update
|
|
180
|
+
try {
|
|
181
|
+
execFileSync('claude', ['plugin', 'marketplace', 'update', marketplaceName], { stdio: 'inherit' });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
|
|
184
|
+
console.warn(`\nWARN: Failed to update marketplace ${marketplaceName} (${exitInfo}) — using cached version`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return plugins.map(plugin => {
|
|
188
|
+
const key = `${plugin}@${marketplaceName}`;
|
|
189
|
+
const isInstalled = Object.hasOwn(installed, key); // Object.hasOwn avoids prototype chain traversal (SCD-7)
|
|
190
|
+
const action = isInstalled ? 'update' : 'install';
|
|
191
|
+
const args = isInstalled
|
|
192
|
+
? ['plugin', 'update', key]
|
|
193
|
+
: ['plugin', 'install', key, '--scope', 'user'];
|
|
194
|
+
try {
|
|
195
|
+
execFileSync('claude', args, { stdio: 'inherit' });
|
|
196
|
+
return { plugin, action, ok: true };
|
|
197
|
+
} catch (error) {
|
|
198
|
+
const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
|
|
199
|
+
console.error(`\nFailed: ${key} ${action} (${exitInfo})`);
|
|
200
|
+
return { plugin, action, ok: false };
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function installMarketplacePlugins() {
|
|
206
|
+
const configPath = join(__dir, 'marketplace.json');
|
|
207
|
+
if (!existsSync(configPath)) return [];
|
|
208
|
+
|
|
209
|
+
// Parse marketplace.json — all failures are non-fatal (SCD-7)
|
|
210
|
+
const config = readJsonObject(configPath, null, 'marketplace.json');
|
|
211
|
+
const marketplaces = config?.marketplaces;
|
|
212
|
+
if (!Array.isArray(marketplaces) || marketplaces.length === 0) {
|
|
213
|
+
console.warn('\nWARN: marketplace.json has no valid "marketplaces" array, skipping');
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Load shared state once: known registry (for source verification) and installed plugins
|
|
218
|
+
const knownPath = join(homedir(), '.claude', 'plugins', 'known_marketplaces.json');
|
|
219
|
+
const knownExists = existsSync(knownPath);
|
|
220
|
+
const known = readJsonObject(knownPath, knownExists ? null : {}, 'known_marketplaces.json');
|
|
221
|
+
if (knownExists && known === null) {
|
|
222
|
+
console.error('\nInvalid known_marketplaces.json: refusing to register any marketplace automatically');
|
|
223
|
+
return marketplaces.flatMap(e =>
|
|
224
|
+
(e?.plugins ?? []).map(plugin => ({ plugin, action: 'skipped (invalid marketplace registry)', ok: false }))
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const installedPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
229
|
+
const installedData = readJsonObject(installedPath, {}, 'installed_plugins.json');
|
|
230
|
+
// installed_plugins.json v2 nests under "plugins", v1 is flat
|
|
231
|
+
const rawInstalled = installedData.plugins ?? installedData;
|
|
232
|
+
const installed = (typeof rawInstalled === 'object' && rawInstalled !== null && !Array.isArray(rawInstalled))
|
|
233
|
+
? rawInstalled
|
|
234
|
+
: {};
|
|
235
|
+
|
|
236
|
+
return marketplaces.flatMap(entry => installFromMarketplace(entry, known, installed));
|
|
237
|
+
}
|
|
238
|
+
|
|
63
239
|
function setupAutoUpdate() {
|
|
64
240
|
const { version: installedVersion } = JSON.parse(
|
|
65
241
|
readFileSync(join(__dir, 'package.json'), 'utf8')
|
package/marketplace.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"marketplaces": [
|
|
3
|
+
{
|
|
4
|
+
"name": "ai-skill-marketplace",
|
|
5
|
+
"source": "git@github.com:trend-ai-taskforce/ai-skill-marketplace.git",
|
|
6
|
+
"plugins": [
|
|
7
|
+
"wiki-tools",
|
|
8
|
+
"atlassian-tools",
|
|
9
|
+
"google-style-guides",
|
|
10
|
+
"l2-automation",
|
|
11
|
+
"service-doc-generator",
|
|
12
|
+
"claude-on-teams"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "claude-plugins-official",
|
|
17
|
+
"source": "anthropics/claude-plugins-official",
|
|
18
|
+
"plugins": [
|
|
19
|
+
"ralph-loop"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trendai-crem/claude-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Claude Code skills installer for the trendai-crem team",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"cli.js",
|
|
15
|
+
"marketplace.json",
|
|
15
16
|
"skills/"
|
|
16
17
|
],
|
|
17
18
|
"type": "module",
|
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: atlassian-tools
|
|
3
|
-
description: |
|
|
4
|
-
Confluence wiki and Jira issue management. ALWAYS use this skill (not WebFetch)
|
|
5
|
-
for any atlassian.net URL, Confluence page, or Jira issue. Use for: wiki pages,
|
|
6
|
-
documentation, search/find wiki pages, retrieve wiki, create/update/delete pages,
|
|
7
|
-
Jira tickets, issues, bugs, sprints.
|
|
8
|
-
allowed-tools: [Bash, Read, Write, Glob]
|
|
9
|
-
metadata:
|
|
10
|
-
short-description: Confluence and Jira CLI tools with bidirectional sync
|
|
11
|
-
version: "1.5.1"
|
|
12
|
-
author: Hank Liao
|
|
13
|
-
license: MIT
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
# Atlassian Tools
|
|
17
|
-
|
|
18
|
-
## RULES (MUST FOLLOW)
|
|
19
|
-
|
|
20
|
-
1. Run all commands from the skill directory (where this SKILL.md is located)
|
|
21
|
-
2. ALWAYS use `-o <USER_CWD>/.atlassian` BEFORE the subcommand (not after)
|
|
22
|
-
3. Replace `<USER_CWD>` with the user's working directory (where they invoked the skill)
|
|
23
|
-
4. **UPDATE WORKFLOW: Always check local storage first before updating**
|
|
24
|
-
- Before updating any wiki page or Jira issue, check if it exists in local storage
|
|
25
|
-
- If found locally: Edit the local `.md` file, then use `update --file` with that file
|
|
26
|
-
- If not found locally: First `retrieve`/`get` it, then edit the local file, then update
|
|
27
|
-
- **NEVER use inline `-d` or description arguments when a local file exists**
|
|
28
|
-
|
|
29
|
-
Correct: `uv run python confluence_cli.py -o /user/project/.atlassian retrieve 123456`
|
|
30
|
-
Wrong: `uv run python confluence_cli.py retrieve 123456 -o /path` (will fail)
|
|
31
|
-
|
|
32
|
-
## TRIGGERS (When to Use This Skill)
|
|
33
|
-
|
|
34
|
-
ALWAYS use this skill (never WebFetch) when you see:
|
|
35
|
-
- URLs containing `atlassian.net` or `/wiki/`
|
|
36
|
-
- Jira issue keys like `PROJ-123`
|
|
37
|
-
- Keywords: Confluence, wiki, Jira, Atlassian, ticket, issue
|
|
38
|
-
|
|
39
|
-
User intent mapping:
|
|
40
|
-
- "get/download/retrieve wiki/page" → `confluence_cli.py retrieve`
|
|
41
|
-
- "get all child pages" → `confluence_cli.py retrieve -r`
|
|
42
|
-
- "search/find wiki pages" → `confluence_cli.py search`
|
|
43
|
-
- "create wiki page" → `confluence_cli.py create`
|
|
44
|
-
- "update/edit wiki" → **check local first**, then `confluence_cli.py update --file`
|
|
45
|
-
- "get issue/ticket" → `jira_cli.py get`
|
|
46
|
-
- "get epic with children" → `jira_cli.py get -r`
|
|
47
|
-
- "create ticket/issue" → `jira_cli.py create`
|
|
48
|
-
- "update issue" → **check local first**, then `jira_cli.py update --description-file`
|
|
49
|
-
- "add comment" → `jira_cli.py comment`
|
|
50
|
-
- "change status" → `jira_cli.py transition`
|
|
51
|
-
|
|
52
|
-
## UPDATE WORKFLOW (CRITICAL)
|
|
53
|
-
|
|
54
|
-
**Always prefer editing local files over inline arguments.** This ensures content integrity and proper version tracking.
|
|
55
|
-
|
|
56
|
-
### Confluence Page Update
|
|
57
|
-
|
|
58
|
-
1. **Check local storage**: `ls <USER_CWD>/.atlassian/confluence/pages/`
|
|
59
|
-
2. **If page exists locally**:
|
|
60
|
-
- Read: `<USER_CWD>/.atlassian/confluence/pages/<page-title>/<page-title>.md`
|
|
61
|
-
- Edit the markdown file with requested changes
|
|
62
|
-
- Update: `confluence_cli.py update <page_id> --file <USER_CWD>/.atlassian/confluence/pages/<page-title>/<page-title>.md`
|
|
63
|
-
3. **If page NOT found locally**:
|
|
64
|
-
- First retrieve: `confluence_cli.py retrieve <page_id_or_url>`
|
|
65
|
-
- Then follow step 2 (page will now exist at `<USER_CWD>/.atlassian/confluence/pages/<page-title>/`)
|
|
66
|
-
|
|
67
|
-
### Jira Issue Update
|
|
68
|
-
|
|
69
|
-
1. **Check local storage**: `ls <USER_CWD>/.atlassian/jira/issues/`
|
|
70
|
-
2. **If issue exists locally**:
|
|
71
|
-
- Read: `<USER_CWD>/.atlassian/jira/issues/<ISSUE-KEY>/description.md`
|
|
72
|
-
- Edit the markdown file with requested changes
|
|
73
|
-
- Update: `jira_cli.py update <ISSUE-KEY> --description-file <USER_CWD>/.atlassian/jira/issues/<ISSUE-KEY>/description.md`
|
|
74
|
-
3. **If issue NOT found locally**:
|
|
75
|
-
- First retrieve: `jira_cli.py get <issue_key>`
|
|
76
|
-
- Then follow step 2 (issue will now exist at `<USER_CWD>/.atlassian/jira/issues/<ISSUE-KEY>/`)
|
|
77
|
-
|
|
78
|
-
### Why This Matters
|
|
79
|
-
|
|
80
|
-
- Preserves full document context (macros, formatting, images)
|
|
81
|
-
- Enables accurate incremental edits
|
|
82
|
-
- Maintains sync between local cache and remote
|
|
83
|
-
- Avoids data loss from partial inline updates
|
|
84
|
-
|
|
85
|
-
## CONFLUENCE CLI
|
|
86
|
-
|
|
87
|
-
Base: `uv run python confluence_cli.py -o <USER_CWD>/.atlassian <command>`
|
|
88
|
-
|
|
89
|
-
### Commands
|
|
90
|
-
|
|
91
|
-
**retrieve** `<page_id_or_url>` `[-r]` `[-f]`
|
|
92
|
-
Download page to local storage. Accepts page ID or full URL.
|
|
93
|
-
- `-r` recursive (include all child pages)
|
|
94
|
-
- `-f` force re-fetch even if cached
|
|
95
|
-
|
|
96
|
-
**create** `<file>` `-s <space_key>` `[-t <title>]` `[-p <parent_id>]` `[-f markdown|html]`
|
|
97
|
-
Create new page from .md or .html file.
|
|
98
|
-
- `-s` space key (required, or set CONFLUENCE_SPACE_KEY env var)
|
|
99
|
-
- `-t` page title (defaults to filename)
|
|
100
|
-
- `-p` parent page ID (for subpages)
|
|
101
|
-
- `-f` force format (auto-detected from extension)
|
|
102
|
-
|
|
103
|
-
**update** `<page_id>` `--file <file>` `[-t <title>]` `[-m <message>]` `[-f markdown|html]`
|
|
104
|
-
Update existing page. Auto-syncs local cache after update.
|
|
105
|
-
- `--file` source file with new content
|
|
106
|
-
- `-t` new title
|
|
107
|
-
- `-m` version message
|
|
108
|
-
- `-f` force format
|
|
109
|
-
|
|
110
|
-
**sync** `[<page_id>]` `[--all]` — Fetch latest from remote to local cache
|
|
111
|
-
- `<page_id>` page ID or URL (required unless --all is used)
|
|
112
|
-
- `--all` sync all locally cached pages
|
|
113
|
-
**list** — List locally cached pages
|
|
114
|
-
**info** `<page_id>` — Show local page metadata
|
|
115
|
-
**delete** `<page_id>` — Remove from local storage only
|
|
116
|
-
**delete-from-confluence** `<page_id>` `[--local-only]` — Delete from Confluence (and local)
|
|
117
|
-
**storage** — Show storage stats
|
|
118
|
-
|
|
119
|
-
**search** `<query>` `[--limit N]` `[--space KEY]` `[--cursor TOKEN]` `[--retrieve N]` `[--json]`
|
|
120
|
-
Search Confluence pages by keyword using CQL full-text search.
|
|
121
|
-
- `<query>` search text (required)
|
|
122
|
-
- `--limit` results per page (1-50, default: 10)
|
|
123
|
-
- `--space` filter by space key (e.g., DEV, DOCS)
|
|
124
|
-
- `--cursor` pagination cursor from previous search
|
|
125
|
-
- `--retrieve` auto-retrieve top N results to local storage (1-20)
|
|
126
|
-
- `--json` output JSON format instead of table
|
|
127
|
-
|
|
128
|
-
### Examples
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
# Get page by URL
|
|
132
|
-
uv run python confluence_cli.py -o /project/.atlassian retrieve "https://domain.atlassian.net/wiki/spaces/SPACE/pages/123456"
|
|
133
|
-
|
|
134
|
-
# Get page and all children
|
|
135
|
-
uv run python confluence_cli.py -o /project/.atlassian retrieve 123456 -r
|
|
136
|
-
|
|
137
|
-
# Create page from markdown
|
|
138
|
-
uv run python confluence_cli.py -o /project/.atlassian create doc.md -s DDIT -t "API Docs"
|
|
139
|
-
|
|
140
|
-
# Create subpage
|
|
141
|
-
uv run python confluence_cli.py -o /project/.atlassian create doc.md -s DDIT -t "API Docs" -p 789012
|
|
142
|
-
|
|
143
|
-
# Update page (preferred: edit local file first)
|
|
144
|
-
# 1. Check if exists: ls /project/.atlassian/confluence/pages/
|
|
145
|
-
# 2. Edit the local markdown file
|
|
146
|
-
# 3. Update from file:
|
|
147
|
-
uv run python confluence_cli.py -o /project/.atlassian update 123456 --file /project/.atlassian/confluence/pages/API-Docs/API-Docs.md -m "Fixed typos"
|
|
148
|
-
|
|
149
|
-
# Sync all cached pages with latest from Confluence
|
|
150
|
-
uv run python confluence_cli.py -o /project/.atlassian sync --all
|
|
151
|
-
|
|
152
|
-
# Search for pages containing "OAuth"
|
|
153
|
-
uv run python confluence_cli.py -o /project/.atlassian search "OAuth"
|
|
154
|
-
|
|
155
|
-
# Search with limit and space filter
|
|
156
|
-
uv run python confluence_cli.py -o /project/.atlassian search "API documentation" --limit 20 --space DEV
|
|
157
|
-
|
|
158
|
-
# Search and auto-retrieve top 3 results
|
|
159
|
-
uv run python confluence_cli.py -o /project/.atlassian search "deployment guide" --retrieve 3
|
|
160
|
-
|
|
161
|
-
# Search with JSON output for scripting
|
|
162
|
-
uv run python confluence_cli.py -o /project/.atlassian search "testing" --json
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## JIRA CLI
|
|
166
|
-
|
|
167
|
-
Base: `uv run python jira_cli.py -o <USER_CWD>/.atlassian <command>`
|
|
168
|
-
|
|
169
|
-
### Commands
|
|
170
|
-
|
|
171
|
-
**get** `<issue_key>` `[-r]` `[-f]`
|
|
172
|
-
Download issue to local storage.
|
|
173
|
-
- `-r` recursive (include all child issues)
|
|
174
|
-
- `-f` force re-fetch even if cached
|
|
175
|
-
|
|
176
|
-
**create** `<project>` `<type>` `<summary>` `[-d <desc>]` `[--description-file <file>]` `[--parent <key>]` `[-p <priority>]` `[-l <labels>]` `[--no-sprint]`
|
|
177
|
-
Create new issue. Auto-saves to local storage. Auto-adds to sprint and applies default labels if configured.
|
|
178
|
-
- `<project>` project key (e.g., PROJ)
|
|
179
|
-
- `<type>` issue type (Task, Bug, Story, Epic)
|
|
180
|
-
- `<summary>` issue title
|
|
181
|
-
- `-d` description text
|
|
182
|
-
- `--description-file` markdown file for description
|
|
183
|
-
- `--parent` parent issue key
|
|
184
|
-
- `-p` priority (Highest, High, Medium, Low, Lowest)
|
|
185
|
-
- `-l` comma-separated labels (merged with JIRA_DEFAULT_LABELS)
|
|
186
|
-
- `--no-sprint` skip auto-adding to sprint
|
|
187
|
-
|
|
188
|
-
**update** `<issue_key>` `[-s <summary>]` `[-d <desc>]` `[--description-file <file>]` `[-p <priority>]` `[-l <labels>]`
|
|
189
|
-
Update issue fields. Auto-syncs local cache.
|
|
190
|
-
- `--description-file` markdown file for description (**preferred over -d**)
|
|
191
|
-
|
|
192
|
-
**comment** `<issue_key>` `<text>` — Add comment. Auto-syncs local cache.
|
|
193
|
-
|
|
194
|
-
**transition** `<issue_key>` `<status>` — Change status (e.g., "In Progress", "Done"). Auto-syncs local cache.
|
|
195
|
-
|
|
196
|
-
**assign-to-me** `<issue_key>` — Assign to current user. Auto-syncs local cache.
|
|
197
|
-
|
|
198
|
-
**set-parent** `<issue_key>` `[parent_key]` `[--clear]` — Set or clear parent relationship
|
|
199
|
-
|
|
200
|
-
**search** `<jql>` `[-l <limit>]` — Search with JQL query
|
|
201
|
-
- `-l` max results (default 50)
|
|
202
|
-
|
|
203
|
-
**types** `<project>` — List available issue types for project
|
|
204
|
-
|
|
205
|
-
**add-to-sprint** `<issue_keys>` `[-s <sprint_name>]` `[-p <project>]`
|
|
206
|
-
Add issues to a sprint.
|
|
207
|
-
- `<issue_keys>` comma-separated issue keys (e.g., PROJ-1,PROJ-2)
|
|
208
|
-
- `-s` sprint name (uses JIRA_SPRINT_NAME_PATTERN if not specified)
|
|
209
|
-
- `-p` project key (defaults to JIRA_PROJECT_KEY)
|
|
210
|
-
|
|
211
|
-
**add-label** `<issue_keys>` `<labels>`
|
|
212
|
-
Add labels to multiple issues.
|
|
213
|
-
- `<issue_keys>` comma-separated issue keys
|
|
214
|
-
- `<labels>` comma-separated labels to add
|
|
215
|
-
|
|
216
|
-
**delete-issue** `<issue_key>` — Delete from Jira (and local if present)
|
|
217
|
-
**list** — List locally cached issues
|
|
218
|
-
**info** `<issue_key>` — Show local issue details
|
|
219
|
-
**sync** `[<issue_key>]` `[--all]` — Fetch latest from remote
|
|
220
|
-
- `<issue_key>` issue key (required unless --all is used)
|
|
221
|
-
- `--all` sync all locally cached issues
|
|
222
|
-
**delete** `<issue_key>` — Remove from local storage only
|
|
223
|
-
**storage** — Show storage stats
|
|
224
|
-
|
|
225
|
-
### Examples
|
|
226
|
-
|
|
227
|
-
```bash
|
|
228
|
-
# Get single issue
|
|
229
|
-
uv run python jira_cli.py -o /project/.atlassian get PROJ-123
|
|
230
|
-
|
|
231
|
-
# Get epic with all children
|
|
232
|
-
uv run python jira_cli.py -o /project/.atlassian get PROJ-100 -r
|
|
233
|
-
|
|
234
|
-
# Create task
|
|
235
|
-
uv run python jira_cli.py create PROJ Task "Implement feature" -d "Description here"
|
|
236
|
-
|
|
237
|
-
# Create from spec file
|
|
238
|
-
uv run python jira_cli.py create PROJ Story "User auth" --description-file spec.md --parent PROJ-100
|
|
239
|
-
|
|
240
|
-
# Update issue (preferred: edit local file first)
|
|
241
|
-
# 1. Check if exists: ls /project/.atlassian/jira/issues/PROJ-123/
|
|
242
|
-
# 2. Edit: /project/.atlassian/jira/issues/PROJ-123/description.md
|
|
243
|
-
# 3. Update from file:
|
|
244
|
-
uv run python jira_cli.py -o /project/.atlassian update PROJ-123 --description-file /project/.atlassian/jira/issues/PROJ-123/description.md
|
|
245
|
-
|
|
246
|
-
# Update issue (simple field changes only)
|
|
247
|
-
uv run python jira_cli.py -o /project/.atlassian update PROJ-123 -s "New title" -p High
|
|
248
|
-
|
|
249
|
-
# Add comment
|
|
250
|
-
uv run python jira_cli.py comment PROJ-123 "Fixed in commit abc123"
|
|
251
|
-
|
|
252
|
-
# Change status
|
|
253
|
-
uv run python jira_cli.py transition PROJ-123 "In Progress"
|
|
254
|
-
|
|
255
|
-
# Search
|
|
256
|
-
uv run python jira_cli.py search "project = PROJ AND status = 'To Do'" -l 20
|
|
257
|
-
|
|
258
|
-
# Sync all cached issues with latest from Jira
|
|
259
|
-
uv run python jira_cli.py -o /project/.atlassian sync --all
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
## REFERENCE
|
|
263
|
-
|
|
264
|
-
### Format Support
|
|
265
|
-
- Input: Markdown (.md) or HTML (.html), auto-detected by extension
|
|
266
|
-
- Output: Pages saved as both .html and .md
|
|
267
|
-
- Supported: headings, lists, tables, code blocks, links, bold/italic, Mermaid diagrams
|
|
268
|
-
|
|
269
|
-
### Confluence Macros & Elements
|
|
270
|
-
|
|
271
|
-
**Fully Converted (lossless round-trip):**
|
|
272
|
-
- Code blocks → markdown fenced blocks (```language)
|
|
273
|
-
- Mermaid diagrams → ```mermaid blocks
|
|
274
|
-
- Status macros → `<!-- status: TITLE ;; colour: COLOR -->`
|
|
275
|
-
- Jira macros → `<!-- jira: KEY ;; server: NAME ;; serverId: ID -->`
|
|
276
|
-
- Expand macros → `<details data-confluence-expand="true"><summary>Title</summary>content</details>`
|
|
277
|
-
- User mentions → `<!-- @user:ACCOUNT_ID -->`
|
|
278
|
-
- Page links → `<!-- @page:SPACE|TITLE|DISPLAY_TEXT -->`
|
|
279
|
-
- Emoticons → `<!-- @emoji:NAME|ID|SHORTNAME -->`
|
|
280
|
-
- Images → `<!-- confluence-image filename: NAME ac:width: N ... -->`
|
|
281
|
-
|
|
282
|
-
**Preserved as Comments:**
|
|
283
|
-
- TOC, Children, Info, Warning, Note, and other macros
|
|
284
|
-
- Format: `<!-- confluence-macro: toc\nstyle: none\ncontent:\n-->`
|
|
285
|
-
|
|
286
|
-
**Special Handling:**
|
|
287
|
-
- Pipe characters in link text auto-escaped for markdown table compatibility
|
|
288
|
-
- Code blocks inside expand macros preserved and restored
|
|
289
|
-
|
|
290
|
-
### Storage Structure
|
|
291
|
-
|
|
292
|
-
```text
|
|
293
|
-
<USER_CWD>/.atlassian/
|
|
294
|
-
├── confluence/pages/<page-title>/
|
|
295
|
-
│ ├── metadata.json
|
|
296
|
-
│ ├── <page-title>.html
|
|
297
|
-
│ └── <page-title>.md
|
|
298
|
-
└── jira/issues/<ISSUE-KEY>/
|
|
299
|
-
├── metadata.json
|
|
300
|
-
├── full_data.json
|
|
301
|
-
└── description.md
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
Recursive retrieval creates nested folders for parent/child relationships.
|
|
305
|
-
|
|
306
|
-
### Credentials
|
|
307
|
-
|
|
308
|
-
Configure in `~/.claude/settings.json`:
|
|
309
|
-
|
|
310
|
-
```json
|
|
311
|
-
{
|
|
312
|
-
"env": {
|
|
313
|
-
"ATLASSIAN_DOMAIN": "trendmicro.atlassian.net",
|
|
314
|
-
"ATLASSIAN_EMAIL": "your-email@trendmicro.com",
|
|
315
|
-
"ATLASSIAN_API_TOKEN": "your_api_token",
|
|
316
|
-
"CONFLUENCE_SPACE_KEY": "MYSPACE",
|
|
317
|
-
"JIRA_PROJECT_KEY": "PROJ",
|
|
318
|
-
"JIRA_SPRINT_NAME_PATTERN": "📜 TLC Sprint *",
|
|
319
|
-
"JIRA_DEFAULT_LABELS": "tpa,backend"
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
Get API token: <https://id.atlassian.com/manage-profile/security/api-tokens>
|
|
325
|
-
|
|
326
|
-
## Version History
|
|
327
|
-
|
|
328
|
-
### v1.5.0 (2025-12-24)
|
|
329
|
-
- **Enhanced**: Lossless round-trip conversion for Confluence elements
|
|
330
|
-
- Status macros (with colour parameter)
|
|
331
|
-
- Jira issue macros (with server/serverId)
|
|
332
|
-
- Expand/collapse macros (full content preservation)
|
|
333
|
-
- User mentions (@user)
|
|
334
|
-
- Page links (cross-space supported)
|
|
335
|
-
- Emoticons (with emoji-id and shortname)
|
|
336
|
-
- Image attachments (all attributes preserved)
|
|
337
|
-
- **Fixed**: Pipe characters in link text breaking markdown tables
|
|
338
|
-
- **Fixed**: Code blocks inside expand macros now properly restored
|
|
339
|
-
- **Fixed**: Image restoration pattern for single-line format
|
|
340
|
-
|
|
341
|
-
### v1.4.0 (2025-12-23)
|
|
342
|
-
- **Added**: `search` command for Confluence pages using CQL full-text search
|
|
343
|
-
- Search by keyword with ranked results
|
|
344
|
-
- Pagination support via `--cursor`
|
|
345
|
-
- Filter by space with `--space`
|
|
346
|
-
- Auto-retrieve top N results with `--retrieve`
|
|
347
|
-
- JSON output format with `--json`
|
|
348
|
-
|
|
349
|
-
### v1.3.0 (2025-12-22)
|
|
350
|
-
- **Added**: `--all` flag to `sync` command for batch operations
|
|
351
|
-
|
|
352
|
-
### v1.2.0
|
|
353
|
-
- **Added**: Project-level storage with configurable `-o` flag
|
|
354
|
-
- **Added**: Hierarchical Jira issue storage
|
|
355
|
-
|
|
356
|
-
### v1.1.0
|
|
357
|
-
- **Added**: Recursive page retrieval with `-r` flag
|
|
358
|
-
- **Added**: HTML/Markdown conversion with macro preservation
|
|
359
|
-
|
|
360
|
-
### v1.0.0
|
|
361
|
-
- Initial release with Confluence and Jira CLI tools
|
|
Binary file
|