acli-axi 0.1.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/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/acli.js +39 -0
- package/dist/auth.js +6 -0
- package/dist/commands/create.js +101 -0
- package/dist/commands/edit.js +51 -0
- package/dist/commands/home.js +28 -0
- package/dist/commands/search.js +40 -0
- package/dist/commands/sprints.js +43 -0
- package/dist/commands/view.js +26 -0
- package/dist/fields.js +47 -0
- package/dist/flags.js +34 -0
- package/dist/help.js +45 -0
- package/dist/index.js +17 -0
- package/dist/payload.js +60 -0
- package/dist/skill-build.js +13 -0
- package/dist/skill.js +84 -0
- package/package.json +57 -0
- package/skills/acli-axi/SKILL.md +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mathieu Neron
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<h1 align="center">acli-axi</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/acli-axi"><img alt="npm" src="https://img.shields.io/npm/v/acli-axi?style=flat-square" /></a>
|
|
5
|
+
<a href="https://github.com/mathieu-neron/acli-axi/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/mathieu-neron/acli-axi/ci.yml?style=flat-square&label=ci" /></a>
|
|
6
|
+
<a href="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-blue?style=flat-square"><img alt="Platform" src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-blue?style=flat-square" /></a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
Jira CLI for agents — designed with [AXI](https://github.com/kunchenguid/axi) (Agent eXperience Interface).
|
|
10
|
+
|
|
11
|
+
Wraps the official [Atlassian CLI (`acli`)](https://developer.atlassian.com/cloud/acli/) with token-efficient TOON output, definitive totals, contextual next-step suggestions, and structured error handling.
|
|
12
|
+
Built for autonomous agents that interact with Jira via shell execution.
|
|
13
|
+
|
|
14
|
+
## Measured gains
|
|
15
|
+
|
|
16
|
+
Jira's REST payloads are built for rendering UIs: avatar URLs in four sizes, `self` links on every entity, status categories with color names.
|
|
17
|
+
`acli --json` passes them through verbatim; an agent reads about six scalars per work item.
|
|
18
|
+
acli-axi flattens each item to those scalars and emits TOON.
|
|
19
|
+
|
|
20
|
+
Measured on a live Jira Cloud site (same query and filters both sides, single run, 2026-07-13):
|
|
21
|
+
|
|
22
|
+
| Command | acli --json | acli-axi | Reduction |
|
|
23
|
+
| ------------------- | ----------- | -------- | ---------- |
|
|
24
|
+
| `search` (25 items) | 71,947 ch | 4,983 ch | **−93.1%** |
|
|
25
|
+
| `search` (5 items) | 15,181 ch | 744 ch | **−95.1%** |
|
|
26
|
+
| `view` (1 item) | 3,619 ch | 841 ch | −76.8% |
|
|
27
|
+
| `sprints` | 653 ch | 240 ch | −63.2% |
|
|
28
|
+
|
|
29
|
+
Latency is parity (mean +18 ms on a ~1.2–1.8 s Jira round-trip).
|
|
30
|
+
The gain scales with result count — per-item overhead is what gets flattened — and compounds in agent sessions, where tool output is re-read as input on every subsequent turn.
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
Install the acli-axi skill in the [Agent Skills](https://agentskills.io) format with [`npx skills`](https://github.com/vercel-labs/skills):
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npx skills add mathieu-neron/acli-axi --skill acli-axi -g
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The skill teaches your agent when to reach for acli-axi and how to install the CLI (`npm install -g acli-axi`, with `npx -y acli-axi` for one-off runs).
|
|
41
|
+
You still need [`acli`](https://developer.atlassian.com/cloud/acli/guides/install-acli/) installed and authenticated via `acli jira auth login --web` (Node 20+ required).
|
|
42
|
+
|
|
43
|
+
The skill is not a user-facing slash command (`user-invocable: false`).
|
|
44
|
+
Just ask for anything that touches Jira — searching or triaging work items with JQL, filing stories or bugs, editing summaries or assignees, or checking a board's sprints — and the agent loads the skill on its own when it recognizes the task.
|
|
45
|
+
|
|
46
|
+
`-g` installs the skill for all projects (`~/.claude/skills/`, for example); drop it to install for the current project only (`.claude/skills/`).
|
|
47
|
+
|
|
48
|
+
## Other Ways to Install
|
|
49
|
+
|
|
50
|
+
The skill is the recommended path, but it is not the only one.
|
|
51
|
+
|
|
52
|
+
### Global install
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npm install -g acli-axi
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Upgrade later with the built-in `acli-axi update` (or `acli-axi update --check` to only look).
|
|
59
|
+
|
|
60
|
+
### Zero setup
|
|
61
|
+
|
|
62
|
+
acli-axi is an AXI, so any capable agent can run the CLI directly with nothing installed at all.
|
|
63
|
+
Just tell your agent:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
Execute `npx -y acli-axi` to get Jira tools.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### From source
|
|
70
|
+
|
|
71
|
+
Clone and pack a tarball:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
git clone https://github.com/mathieu-neron/acli-axi.git
|
|
75
|
+
cd acli-axi && npm pack && npm install -g ./acli-axi-*.tgz
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Do not `npm install -g` the git URL directly** — on npm 10/11 a global git install can be reified as a symlink into npm's temporary clone, which npm deletes after install, leaving a broken binary that still reported success. Tarball installs copy files and cannot hit this. Verify any install with `acli-axi --version`.
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
acli-axi # home - auth state + your open work items, no args needed
|
|
84
|
+
acli-axi search --jql "project = TEAM AND sprint in openSprints()"
|
|
85
|
+
acli-axi search --jql "..." --limit 50 --fields key,summary,labels
|
|
86
|
+
acli-axi view TEAM-123 # long descriptions truncated; --full to expand
|
|
87
|
+
acli-axi sprints --board 36 # active + future sprints by default
|
|
88
|
+
acli-axi create --project TEAM --type Story --summary "New thing" --assignee @me
|
|
89
|
+
acli-axi create --project TEAM --type Story --summary "..." \
|
|
90
|
+
--parent TEAM-93 -f customfield_10028=1 -f customfield_10020=11144 # custom fields (points, sprint)
|
|
91
|
+
acli-axi edit TEAM-123 --summary "Better title" # never prompts (--yes under the hood)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
List output reports the **true total**, not the page size: when a search fills its `--limit`, acli-axi runs acli's `--count` behind the scenes and tells you `total: 14724, showing: 25` with a hint to raise the limit.
|
|
95
|
+
|
|
96
|
+
Custom fields (`-f`/`--field <id>=<value>`, repeatable) are **create-only**: acli's edit schema rejects them, and acli-axi says so with a structured error instead of failing cryptically.
|
|
97
|
+
Values that parse as JSON become numbers or arrays; everything else stays a string.
|
|
98
|
+
|
|
99
|
+
### Commands
|
|
100
|
+
|
|
101
|
+
| Command | Description |
|
|
102
|
+
| --------- | --------------------------------------------------------------------- |
|
|
103
|
+
| `search` | Search work items with JQL — flattened rows, definitive totals |
|
|
104
|
+
| `view` | Full detail for one work item — ADF descriptions as plain text |
|
|
105
|
+
| `create` | Create a work item — flags plus `-f` custom fields, echoes invariants |
|
|
106
|
+
| `edit` | Edit a work item — summary, assignee, labels, type; never prompts |
|
|
107
|
+
| `sprints` | List a board's sprints — active + future by default |
|
|
108
|
+
| `update` | Built-in self-update command inherited from `axi-sdk-js` |
|
|
109
|
+
|
|
110
|
+
### Global flags
|
|
111
|
+
|
|
112
|
+
- `--help` — show help for any command
|
|
113
|
+
- `-v`, `-V`, `--version` — show the installed `acli-axi` version
|
|
114
|
+
|
|
115
|
+
Errors are structured (`error:` + `code:` + `help:`): exit 2 for usage errors, exit 1 for everything else; stdout is data, stderr is diagnostics.
|
|
116
|
+
|
|
117
|
+
## Scope
|
|
118
|
+
|
|
119
|
+
v0.1 covers Jira core, plus the installable Agent Skill above.
|
|
120
|
+
Not wrapped yet (use `acli` directly): comments, transitions, attachments, links, projects, filters, Confluence.
|
|
121
|
+
Also deferred: session hooks (`installSessionStartHooks` in the SDK).
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
pnpm install
|
|
127
|
+
pnpm run build # Compile TypeScript to dist/
|
|
128
|
+
pnpm run build:skill # Regenerate skills/acli-axi/SKILL.md from the CLI's own help
|
|
129
|
+
pnpm test # Run tests with vitest (the acli boundary is mocked)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The committed `skills/acli-axi/SKILL.md` is generated by `pnpm run build:skill`; `pnpm test` fails if it drifts from the generator — never hand-edit it.
|
|
133
|
+
The npm package includes `skills/acli-axi/`, so published releases ship the same installable Agent Skill documented in Quick Start.
|
|
134
|
+
|
|
135
|
+
Built on [`axi-sdk-js`](https://www.npmjs.com/package/axi-sdk-js), which provides command dispatch, TOON serialization, structured errors, and the `update` command.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
[MIT](LICENSE)
|
package/dist/acli.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { AxiError } from "axi-sdk-js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
export async function acli(args) {
|
|
6
|
+
try {
|
|
7
|
+
const { stdout } = await execFileAsync("acli", args, { maxBuffer: 64 * 1024 * 1024 });
|
|
8
|
+
return stdout;
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
throw toAxiError(error);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function acliJson(args) {
|
|
15
|
+
const stdout = await acli([...args, "--json"]);
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(stdout);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new AxiError(`acli returned non-JSON output: ${stdout.slice(0, 200)}`, "ACLI_ERROR", ["Check that the installed acli version supports --json for this command"]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function toAxiError(error) {
|
|
24
|
+
if (error instanceof AxiError) {
|
|
25
|
+
return error;
|
|
26
|
+
}
|
|
27
|
+
const e = error;
|
|
28
|
+
if (e.code === "ENOENT") {
|
|
29
|
+
return new AxiError("acli is not installed or not on PATH", "ACLI_NOT_FOUND", [
|
|
30
|
+
"Install the Atlassian CLI: https://developer.atlassian.com/cloud/acli/guides/install-acli/",
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
const raw = [e.stderr, e.stdout].filter(Boolean).join("\n").trim() || e.message || "acli failed";
|
|
34
|
+
const message = raw.replace(/^✗\s*Error:\s*/m, "").trim();
|
|
35
|
+
if (/not authenticated|unauthenticated|log ?in|token expired/i.test(message)) {
|
|
36
|
+
return new AxiError(message, "AUTH_ERROR", ["Run `acli jira auth login --web` to authenticate"]);
|
|
37
|
+
}
|
|
38
|
+
return new AxiError(message, "ACLI_ERROR");
|
|
39
|
+
}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { acli, acliJson } from "../acli.js";
|
|
3
|
+
import { parseAuthStatus } from "../auth.js";
|
|
4
|
+
import { flatten } from "../fields.js";
|
|
5
|
+
import { parseFlags, requireFlag } from "../flags.js";
|
|
6
|
+
import { parseFieldPairs, toAdf, withJsonFile } from "../payload.js";
|
|
7
|
+
const EXAMPLE = 'acli-axi create --project TEAM --type Story --summary "New thing"';
|
|
8
|
+
const PASSTHROUGH = ["description", "assignee", "label", "parent"];
|
|
9
|
+
export async function create(args, _context) {
|
|
10
|
+
const { values } = parseFlags(args, {
|
|
11
|
+
project: { type: "string" },
|
|
12
|
+
type: { type: "string" },
|
|
13
|
+
summary: { type: "string" },
|
|
14
|
+
description: { type: "string" },
|
|
15
|
+
assignee: { type: "string" },
|
|
16
|
+
label: { type: "string" },
|
|
17
|
+
parent: { type: "string" },
|
|
18
|
+
field: { type: "string", multiple: true, short: "f" },
|
|
19
|
+
}, EXAMPLE);
|
|
20
|
+
const project = requireFlag(values.project, "project", EXAMPLE);
|
|
21
|
+
const type = requireFlag(values.type, "type", EXAMPLE);
|
|
22
|
+
const summary = requireFlag(values.summary, "summary", EXAMPLE);
|
|
23
|
+
const custom = parseFieldPairs(values.field ?? []);
|
|
24
|
+
const result = Object.keys(custom).length
|
|
25
|
+
? await createFromJson(values, project, type, summary, custom)
|
|
26
|
+
: await createFromFlags(values, project, type, summary);
|
|
27
|
+
if (!result.key) {
|
|
28
|
+
throw new AxiError("acli did not return a work item key; the create may or may not have succeeded", "ACLI_ERROR", [`Verify with: acli-axi search --jql "project = ${project} ORDER BY created DESC" --limit 5`]);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
created: result.key,
|
|
32
|
+
...echoInvariants(result.fields, values, custom),
|
|
33
|
+
help: [`Run \`acli-axi view ${result.key}\` to inspect it`],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Plain flag path: everything acli exposes as flags. Kept separate because it is
|
|
37
|
+
// the live-verified surface; JSON is only needed for custom fields.
|
|
38
|
+
async function createFromFlags(values, project, type, summary) {
|
|
39
|
+
const acliArgs = ["jira", "workitem", "create", "--project", project, "--type", type, "--summary", summary];
|
|
40
|
+
for (const flag of PASSTHROUGH) {
|
|
41
|
+
if (typeof values[flag] === "string") {
|
|
42
|
+
acliArgs.push(`--${flag}`, values[flag]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return acliJson(acliArgs);
|
|
46
|
+
}
|
|
47
|
+
// Custom fields (sprint, story points, ...) have no acli flag; --from-json is
|
|
48
|
+
// acli's only door to them. The temp payload never enters agent context.
|
|
49
|
+
async function createFromJson(values, project, type, summary, custom) {
|
|
50
|
+
const payload = {
|
|
51
|
+
projectKey: project,
|
|
52
|
+
type,
|
|
53
|
+
summary,
|
|
54
|
+
additionalAttributes: custom,
|
|
55
|
+
};
|
|
56
|
+
if (typeof values.assignee === "string")
|
|
57
|
+
payload.assignee = await resolveAssignee(values.assignee);
|
|
58
|
+
if (typeof values.parent === "string")
|
|
59
|
+
payload.parentIssueId = values.parent;
|
|
60
|
+
if (typeof values.label === "string")
|
|
61
|
+
payload.labels = values.label.split(",");
|
|
62
|
+
if (typeof values.description === "string")
|
|
63
|
+
payload.description = toAdf(values.description);
|
|
64
|
+
return withJsonFile(payload, (path) => acliJson(["jira", "workitem", "create", "--from-json", path]));
|
|
65
|
+
}
|
|
66
|
+
// acli's --assignee flag resolves @me itself, but --from-json treats the
|
|
67
|
+
// assignee string as a user search — a literal "@me" in the payload assigned
|
|
68
|
+
// an unrelated user on a live site. Resolve it to the authed email up front.
|
|
69
|
+
async function resolveAssignee(assignee) {
|
|
70
|
+
if (assignee !== "@me") {
|
|
71
|
+
return assignee;
|
|
72
|
+
}
|
|
73
|
+
const email = parseAuthStatus(await acli(["jira", "auth", "status"])).email;
|
|
74
|
+
if (!email) {
|
|
75
|
+
throw new AxiError("could not resolve @me: no authenticated email in `acli jira auth status`", "AUTH_ERROR", ["Run `acli jira auth login --web` to authenticate, or pass an explicit email/accountId"]);
|
|
76
|
+
}
|
|
77
|
+
return email;
|
|
78
|
+
}
|
|
79
|
+
// acli echoes the full created issue; reflect back the invariants the caller
|
|
80
|
+
// set so verification costs zero extra calls.
|
|
81
|
+
function echoInvariants(fields, values, custom) {
|
|
82
|
+
if (!fields) {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
const echo = {};
|
|
86
|
+
const issuetype = fields.issuetype;
|
|
87
|
+
if (issuetype?.name)
|
|
88
|
+
echo.type = issuetype.name;
|
|
89
|
+
const parent = fields.parent;
|
|
90
|
+
if (values.parent && parent?.key)
|
|
91
|
+
echo.parent = parent.key;
|
|
92
|
+
const assignee = fields.assignee;
|
|
93
|
+
if (values.assignee && assignee?.displayName)
|
|
94
|
+
echo.assignee = assignee.displayName;
|
|
95
|
+
for (const id of Object.keys(custom)) {
|
|
96
|
+
if (fields[id] !== undefined && fields[id] !== null) {
|
|
97
|
+
echo[id] = flatten(fields[id]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return echo;
|
|
101
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { acli } from "../acli.js";
|
|
3
|
+
import { parseFlags } from "../flags.js";
|
|
4
|
+
import { parseFieldPairs } from "../payload.js";
|
|
5
|
+
const EXAMPLE = 'acli-axi edit TEAM-123 --summary "Better title"';
|
|
6
|
+
const CHANGE_FLAGS = ["summary", "description", "assignee", "labels", "type"];
|
|
7
|
+
export async function edit(args, _context) {
|
|
8
|
+
const { values, positionals } = parseFlags(args, {
|
|
9
|
+
summary: { type: "string" },
|
|
10
|
+
description: { type: "string" },
|
|
11
|
+
assignee: { type: "string" },
|
|
12
|
+
labels: { type: "string" },
|
|
13
|
+
type: { type: "string" },
|
|
14
|
+
"remove-assignee": { type: "boolean" },
|
|
15
|
+
field: { type: "string", multiple: true, short: "f" },
|
|
16
|
+
}, EXAMPLE, 1);
|
|
17
|
+
const key = positionals[0];
|
|
18
|
+
if (!key) {
|
|
19
|
+
throw new AxiError("A work item key is required", "VALIDATION_ERROR", [`Example: ${EXAMPLE}`]);
|
|
20
|
+
}
|
|
21
|
+
const acliArgs = ["jira", "workitem", "edit", "--key", key];
|
|
22
|
+
let changed = false;
|
|
23
|
+
for (const flag of CHANGE_FLAGS) {
|
|
24
|
+
if (typeof values[flag] === "string") {
|
|
25
|
+
acliArgs.push(`--${flag}`, values[flag]);
|
|
26
|
+
changed = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (values["remove-assignee"]) {
|
|
30
|
+
acliArgs.push("--remove-assignee");
|
|
31
|
+
changed = true;
|
|
32
|
+
}
|
|
33
|
+
const custom = parseFieldPairs(values.field ?? []);
|
|
34
|
+
if (!changed && Object.keys(custom).length === 0) {
|
|
35
|
+
throw new AxiError("At least one change flag is required", "VALIDATION_ERROR", [
|
|
36
|
+
`Example: ${EXAMPLE}`,
|
|
37
|
+
`Change flags: ${CHANGE_FLAGS.map((f) => `--${f}`).join(", ")}, --remove-assignee, --field <id>=<value>`,
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
if (Object.keys(custom).length > 0) {
|
|
41
|
+
// acli's edit --from-json schema strictly rejects additionalAttributes
|
|
42
|
+
// (verified live, acli 1.3.22) — custom fields are create-only in acli.
|
|
43
|
+
throw new AxiError("acli cannot edit custom fields on an existing work item (its edit schema rejects additionalAttributes)", "VALIDATION_ERROR", [
|
|
44
|
+
"Custom fields can only be set at creation: acli-axi create ... -f <id>=<value>",
|
|
45
|
+
"To fix a wrong custom field, edit it in the Jira UI or recreate the work item",
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
// --yes: acli prompts for confirmation otherwise, which would hang an agent
|
|
49
|
+
await acli([...acliArgs, "--yes"]);
|
|
50
|
+
return { edited: key, help: [`Run \`acli-axi view ${key}\` to verify`] };
|
|
51
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { acli, acliJson } from "../acli.js";
|
|
2
|
+
import { parseAuthStatus } from "../auth.js";
|
|
3
|
+
import { flattenFields } from "../fields.js";
|
|
4
|
+
const MY_OPEN_JQL = "assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC";
|
|
5
|
+
export async function home(_args, _context) {
|
|
6
|
+
const [status, items] = await Promise.all([
|
|
7
|
+
acli(["jira", "auth", "status"]),
|
|
8
|
+
acliJson([
|
|
9
|
+
"jira", "workitem", "search", "--jql", MY_OPEN_JQL, "--limit", "5", "--fields", "key,status,summary",
|
|
10
|
+
]),
|
|
11
|
+
]);
|
|
12
|
+
const parsed = parseAuthStatus(status);
|
|
13
|
+
const site = parsed.site ?? "unknown";
|
|
14
|
+
const email = parsed.email ?? "unknown";
|
|
15
|
+
const open = items ?? [];
|
|
16
|
+
return {
|
|
17
|
+
auth: { site, email },
|
|
18
|
+
my_open_workitems: open.length
|
|
19
|
+
? open.map((item) => ({ key: item.key, ...flattenFields(item.fields) }))
|
|
20
|
+
: "0 open work items assigned to you",
|
|
21
|
+
help: [
|
|
22
|
+
'acli-axi search --jql "project = TEAM AND sprint in openSprints()"',
|
|
23
|
+
"acli-axi view <KEY>",
|
|
24
|
+
"acli-axi sprints --board <id>",
|
|
25
|
+
'acli-axi create --project TEAM --type Story --summary "..."',
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { acli, acliJson } from "../acli.js";
|
|
3
|
+
import { flattenFields } from "../fields.js";
|
|
4
|
+
import { parseFlags, requireFlag } from "../flags.js";
|
|
5
|
+
const EXAMPLE = 'acli-axi search --jql "project = TEAM AND sprint in openSprints()"';
|
|
6
|
+
export async function search(args, _context) {
|
|
7
|
+
const { values } = parseFlags(args, { jql: { type: "string" }, limit: { type: "string" }, fields: { type: "string" } }, EXAMPLE);
|
|
8
|
+
const jql = requireFlag(values.jql, "jql", EXAMPLE);
|
|
9
|
+
const limit = values.limit ?? "25";
|
|
10
|
+
const acliArgs = ["jira", "workitem", "search", "--jql", jql, "--limit", limit];
|
|
11
|
+
if (typeof values.fields === "string") {
|
|
12
|
+
acliArgs.push("--fields", values.fields);
|
|
13
|
+
}
|
|
14
|
+
const items = (await acliJson(acliArgs)) ?? [];
|
|
15
|
+
if (items.length === 0) {
|
|
16
|
+
return { total: 0, message: "No work items match this JQL query." };
|
|
17
|
+
}
|
|
18
|
+
// A full page means the true total is unknown; one extra --count call makes it definitive.
|
|
19
|
+
const total = items.length >= Number(limit) ? await countMatches(jql) : items.length;
|
|
20
|
+
const truncated = total > items.length;
|
|
21
|
+
return {
|
|
22
|
+
total,
|
|
23
|
+
...(truncated ? { showing: items.length } : {}),
|
|
24
|
+
workitems: items.map((item) => ({ key: item.key, ...flattenFields(item.fields) })),
|
|
25
|
+
help: [
|
|
26
|
+
...(truncated
|
|
27
|
+
? [`Re-run with --limit ${Math.min(total, 500)} to see more of the ${total} matches`]
|
|
28
|
+
: []),
|
|
29
|
+
"Run `acli-axi view <key>` for full details including description",
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function countMatches(jql) {
|
|
34
|
+
const output = await acli(["jira", "workitem", "search", "--jql", jql, "--count"]);
|
|
35
|
+
const match = output.match(/:\s*(\d+)/);
|
|
36
|
+
if (!match) {
|
|
37
|
+
throw new AxiError(`Could not parse total from acli --count output: ${output.slice(0, 120)}`, "ACLI_ERROR");
|
|
38
|
+
}
|
|
39
|
+
return Number(match[1]);
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { acliJson } from "../acli.js";
|
|
2
|
+
import { truncate } from "../fields.js";
|
|
3
|
+
import { parseFlags, requireFlag } from "../flags.js";
|
|
4
|
+
const EXAMPLE = "acli-axi sprints --board 36";
|
|
5
|
+
const GOAL_LIMIT = 120;
|
|
6
|
+
export async function sprints(args, _context) {
|
|
7
|
+
const { values } = parseFlags(args, { board: { type: "string" }, state: { type: "string" }, limit: { type: "string" }, full: { type: "boolean" } }, EXAMPLE);
|
|
8
|
+
const board = requireFlag(values.board, "board", EXAMPLE);
|
|
9
|
+
const state = values.state ?? "active,future";
|
|
10
|
+
const result = await acliJson([
|
|
11
|
+
"jira", "board", "list-sprints", "--id", board, "--state", state, "--limit", values.limit ?? "50",
|
|
12
|
+
]);
|
|
13
|
+
let goalsTruncated = false;
|
|
14
|
+
const rows = (result.sprints ?? []).map((sprint) => {
|
|
15
|
+
const goal = sprint.goal?.replaceAll("\n", " ");
|
|
16
|
+
if (goal && !values.full && goal.length > GOAL_LIMIT) {
|
|
17
|
+
goalsTruncated = true;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
id: sprint.id,
|
|
21
|
+
name: sprint.name,
|
|
22
|
+
state: sprint.state,
|
|
23
|
+
start: sprint.startDate?.slice(0, 10),
|
|
24
|
+
end: sprint.endDate?.slice(0, 10),
|
|
25
|
+
goal: goal && !values.full ? truncate(goal, GOAL_LIMIT) : goal,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
if (rows.length === 0) {
|
|
29
|
+
return { total: 0, message: `No sprints on board ${board} in state: ${state}.` };
|
|
30
|
+
}
|
|
31
|
+
const total = result.total ?? rows.length;
|
|
32
|
+
const truncated = total > rows.length;
|
|
33
|
+
return {
|
|
34
|
+
total,
|
|
35
|
+
...(truncated ? { showing: rows.length } : {}),
|
|
36
|
+
sprints: rows,
|
|
37
|
+
help: [
|
|
38
|
+
...(truncated ? [`Re-run with --limit ${total} to see all ${total} sprints`] : []),
|
|
39
|
+
...(goalsTruncated ? ["Re-run with --full for complete sprint goals"] : []),
|
|
40
|
+
`acli-axi search --jql "sprint = <id>" to list a sprint's work items`,
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { acliJson } from "../acli.js";
|
|
3
|
+
import { flattenFields } from "../fields.js";
|
|
4
|
+
import { parseFlags } from "../flags.js";
|
|
5
|
+
const EXAMPLE = "acli-axi view TEAM-123";
|
|
6
|
+
const DESCRIPTION_LIMIT = 1000;
|
|
7
|
+
export async function view(args, _context) {
|
|
8
|
+
const { values, positionals } = parseFlags(args, { full: { type: "boolean" }, fields: { type: "string" } }, EXAMPLE, 1);
|
|
9
|
+
const key = positionals[0];
|
|
10
|
+
if (!key) {
|
|
11
|
+
throw new AxiError("A work item key is required", "VALIDATION_ERROR", [`Example: ${EXAMPLE}`]);
|
|
12
|
+
}
|
|
13
|
+
const acliArgs = ["jira", "workitem", "view", key];
|
|
14
|
+
if (typeof values.fields === "string") {
|
|
15
|
+
acliArgs.push("--fields", values.fields);
|
|
16
|
+
}
|
|
17
|
+
const item = await acliJson(acliArgs);
|
|
18
|
+
const fields = flattenFields(item.fields);
|
|
19
|
+
const description = fields.description;
|
|
20
|
+
if (typeof description === "string" && !values.full && description.length > DESCRIPTION_LIMIT) {
|
|
21
|
+
fields.description = description.slice(0, DESCRIPTION_LIMIT);
|
|
22
|
+
fields.description_truncated = `showing ${DESCRIPTION_LIMIT} of ${description.length} chars`;
|
|
23
|
+
fields.help = [`Run \`acli-axi view ${key} --full\` for the complete description`];
|
|
24
|
+
}
|
|
25
|
+
return { key: item.key, ...fields };
|
|
26
|
+
}
|
package/dist/fields.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Collapses Jira's deeply nested field payloads (avatar URLs, self links,
|
|
2
|
+
// status categories) into the scalar an agent actually reads.
|
|
3
|
+
export function flattenFields(fields) {
|
|
4
|
+
return Object.fromEntries(Object.entries(fields)
|
|
5
|
+
.map(([key, value]) => [key, flatten(value)])
|
|
6
|
+
.filter(([, value]) => value !== null && value !== undefined));
|
|
7
|
+
}
|
|
8
|
+
export function flatten(value) {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return value.map(flatten);
|
|
11
|
+
}
|
|
12
|
+
if (value === null || typeof value !== "object") {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
const record = value;
|
|
16
|
+
if (record.type === "doc" && Array.isArray(record.content)) {
|
|
17
|
+
return adfToText(record);
|
|
18
|
+
}
|
|
19
|
+
const scalar = record.displayName ?? record.name ?? record.value ?? record.emailAddress;
|
|
20
|
+
if (scalar !== undefined && scalar !== null) {
|
|
21
|
+
return scalar;
|
|
22
|
+
}
|
|
23
|
+
return Object.fromEntries(Object.entries(record)
|
|
24
|
+
.filter(([key]) => !NOISE_KEYS.has(key))
|
|
25
|
+
.map(([key, nested]) => [key, flatten(nested)]));
|
|
26
|
+
}
|
|
27
|
+
// Jira hypermedia noise an agent never reads; keeps custom/--fields '*all'
|
|
28
|
+
// output as lean as the default field set.
|
|
29
|
+
const NOISE_KEYS = new Set(["self", "avatarUrls", "iconUrl", "expand"]);
|
|
30
|
+
// Minimal Atlassian Document Format -> plain text: joins text nodes,
|
|
31
|
+
// newline per block. ponytail: ignores marks/tables/media, add if agents need them.
|
|
32
|
+
function adfToText(node) {
|
|
33
|
+
if (typeof node.text === "string") {
|
|
34
|
+
return node.text;
|
|
35
|
+
}
|
|
36
|
+
if (!Array.isArray(node.content)) {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
const joiner = node.type === "doc" ? "\n" : "";
|
|
40
|
+
return node.content
|
|
41
|
+
.map((child) => adfToText(child))
|
|
42
|
+
.filter((text) => text.length > 0)
|
|
43
|
+
.join(joiner);
|
|
44
|
+
}
|
|
45
|
+
export function truncate(text, max) {
|
|
46
|
+
return text.length > max ? `${text.slice(0, max)}… (${text.length} chars total)` : text;
|
|
47
|
+
}
|
package/dist/flags.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { AxiError } from "axi-sdk-js";
|
|
3
|
+
export function parseFlags(args, options, example, maxPositionals = 0) {
|
|
4
|
+
try {
|
|
5
|
+
const { values, positionals } = parseArgs({ args, options, strict: true, allowPositionals: true });
|
|
6
|
+
if (positionals.length > maxPositionals) {
|
|
7
|
+
throw new AxiError(`Unexpected argument: ${positionals[maxPositionals]}`, "VALIDATION_ERROR", [
|
|
8
|
+
`Valid flags: ${Object.keys(options)
|
|
9
|
+
.map((flag) => `--${flag}`)
|
|
10
|
+
.join(", ")}`,
|
|
11
|
+
`Example: ${example}`,
|
|
12
|
+
]);
|
|
13
|
+
}
|
|
14
|
+
return { values: values, positionals };
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof AxiError) {
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
21
|
+
throw new AxiError(message, "VALIDATION_ERROR", [
|
|
22
|
+
`Valid flags: ${Object.keys(options)
|
|
23
|
+
.map((flag) => `--${flag}`)
|
|
24
|
+
.join(", ")}`,
|
|
25
|
+
`Example: ${example}`,
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function requireFlag(value, flag, example) {
|
|
30
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
31
|
+
throw new AxiError(`--${flag} is required`, "VALIDATION_ERROR", [`Example: ${example}`]);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
export const { version: VERSION, description: DESCRIPTION } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
3
|
+
export const TOP_LEVEL_HELP = `acli-axi — ${DESCRIPTION}
|
|
4
|
+
|
|
5
|
+
commands:
|
|
6
|
+
search --jql <query> [--limit N] [--fields a,b] Search work items
|
|
7
|
+
view <KEY> [--full] [--fields a,b] View one work item
|
|
8
|
+
create --project P --type T --summary S [...] Create a work item
|
|
9
|
+
edit <KEY> --summary|--assignee|--labels|... S Edit a work item
|
|
10
|
+
sprints --board <id> [--state active,future,closed] List board sprints
|
|
11
|
+
|
|
12
|
+
Run \`acli-axi <command> --help\` for flags and examples.
|
|
13
|
+
Requires the Atlassian CLI (acli) installed and authenticated (\`acli jira auth login --web\`).
|
|
14
|
+
`;
|
|
15
|
+
export const COMMAND_HELP = {
|
|
16
|
+
search: `search: query Jira work items with JQL
|
|
17
|
+
flags:
|
|
18
|
+
--jql <query> JQL query (required)
|
|
19
|
+
--limit <n> Max results (default 25)
|
|
20
|
+
--fields <a,b> Comma-separated fields (default: issuetype,key,assignee,priority,status,summary)
|
|
21
|
+
example: acli-axi search --jql "project = TEAM AND sprint in openSprints()"
|
|
22
|
+
`,
|
|
23
|
+
view: `view: full detail for one work item
|
|
24
|
+
usage: acli-axi view <KEY> [--full] [--fields a,b]
|
|
25
|
+
--full Do not truncate long descriptions
|
|
26
|
+
--fields <a,b> Field subset ('*all' for everything)
|
|
27
|
+
example: acli-axi view TEAM-123
|
|
28
|
+
`,
|
|
29
|
+
create: `create: create a work item
|
|
30
|
+
flags: --project (required), --type (required), --summary (required),
|
|
31
|
+
--description, --assignee ('@me' to self-assign), --label, --parent,
|
|
32
|
+
-f/--field <id>=<value> (repeatable; custom fields, e.g. story points, sprint)
|
|
33
|
+
note: --description takes plain text or an ADF doc as JSON. Markdown is never
|
|
34
|
+
converted — it is stored as literal characters.
|
|
35
|
+
example: acli-axi create --project TEAM --type Story --summary "New thing" --assignee @me -f customfield_10028=1
|
|
36
|
+
`,
|
|
37
|
+
edit: `edit: change fields on a work item (never prompts)
|
|
38
|
+
usage: acli-axi edit <KEY> [--summary S] [--description D] [--assignee A] [--labels L] [--type T] [--remove-assignee] [-f/--field <id>=<value>]
|
|
39
|
+
example: acli-axi edit TEAM-123 --summary "Better title"
|
|
40
|
+
`,
|
|
41
|
+
sprints: `sprints: list sprints on a board
|
|
42
|
+
flags: --board <id> (required), --state <s> (default active,future), --limit <n>, --full (untruncated goals)
|
|
43
|
+
example: acli-axi sprints --board 36
|
|
44
|
+
`,
|
|
45
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runAxiCli } from "axi-sdk-js";
|
|
3
|
+
import { COMMAND_HELP, TOP_LEVEL_HELP, VERSION } from "./help.js";
|
|
4
|
+
import { search } from "./commands/search.js";
|
|
5
|
+
import { view } from "./commands/view.js";
|
|
6
|
+
import { create } from "./commands/create.js";
|
|
7
|
+
import { edit } from "./commands/edit.js";
|
|
8
|
+
import { sprints } from "./commands/sprints.js";
|
|
9
|
+
import { home } from "./commands/home.js";
|
|
10
|
+
await runAxiCli({
|
|
11
|
+
description: "Manage Jira work items and sprints via the Atlassian CLI",
|
|
12
|
+
version: VERSION,
|
|
13
|
+
topLevelHelp: TOP_LEVEL_HELP,
|
|
14
|
+
getCommandHelp: (command) => COMMAND_HELP[command],
|
|
15
|
+
home,
|
|
16
|
+
commands: { search, view, create, edit, sprints },
|
|
17
|
+
});
|
package/dist/payload.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { AxiError } from "axi-sdk-js";
|
|
5
|
+
// --field customfield_10028=0.5 → { customfield_10028: 0.5 }
|
|
6
|
+
// Values that parse as JSON become numbers/arrays/objects; everything else stays a string.
|
|
7
|
+
export function parseFieldPairs(pairs) {
|
|
8
|
+
const fields = {};
|
|
9
|
+
for (const pair of pairs) {
|
|
10
|
+
const eq = pair.indexOf("=");
|
|
11
|
+
if (eq < 1) {
|
|
12
|
+
throw new AxiError(`--field must be <id>=<value>, got: ${pair}`, "VALIDATION_ERROR", [
|
|
13
|
+
"Example: --field customfield_10028=0.5",
|
|
14
|
+
]);
|
|
15
|
+
}
|
|
16
|
+
const raw = pair.slice(eq + 1);
|
|
17
|
+
let value = raw;
|
|
18
|
+
try {
|
|
19
|
+
value = JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// not JSON — keep the raw string
|
|
23
|
+
}
|
|
24
|
+
fields[pair.slice(0, eq)] = value;
|
|
25
|
+
}
|
|
26
|
+
return fields;
|
|
27
|
+
}
|
|
28
|
+
export function adfParagraph(text) {
|
|
29
|
+
return {
|
|
30
|
+
type: "doc",
|
|
31
|
+
version: 1,
|
|
32
|
+
content: [{ type: "paragraph", content: [{ type: "text", text }] }],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// acli's own --description flag accepts "plain text or ADF" and detects which it
|
|
36
|
+
// got; --from-json takes only ADF. Mirroring that detection keeps both create
|
|
37
|
+
// branches equivalent — without it, adding any -f flag silently downgraded a
|
|
38
|
+
// caller's ADF doc into the literal JSON text of that doc.
|
|
39
|
+
export function toAdf(text) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(text);
|
|
42
|
+
if (typeof parsed === "object" && parsed !== null && parsed.type === "doc") {
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// not JSON — plain text
|
|
48
|
+
}
|
|
49
|
+
return adfParagraph(text);
|
|
50
|
+
}
|
|
51
|
+
export async function withJsonFile(payload, run) {
|
|
52
|
+
const path = join(tmpdir(), `acli-axi-${process.pid}-${Date.now()}.json`);
|
|
53
|
+
writeFileSync(path, JSON.stringify(payload));
|
|
54
|
+
try {
|
|
55
|
+
return await run(path);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
unlinkSync(path);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Regenerates skills/acli-axi/SKILL.md from the shared CLI guidance.
|
|
2
|
+
// Drift is caught by test/skill.test.ts, which pins the committed file
|
|
3
|
+
// byte-for-byte to createSkillMarkdown().
|
|
4
|
+
//
|
|
5
|
+
// pnpm run build:skill
|
|
6
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { createSkillMarkdown } from "./skill.js";
|
|
9
|
+
const dir = new URL("../skills/acli-axi/", import.meta.url);
|
|
10
|
+
const target = new URL("SKILL.md", dir);
|
|
11
|
+
await mkdir(dir, { recursive: true });
|
|
12
|
+
await writeFile(target, createSkillMarkdown());
|
|
13
|
+
console.log(`Wrote ${fileURLToPath(target)}`);
|
package/dist/skill.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { DESCRIPTION, TOP_LEVEL_HELP } from "./help.js";
|
|
2
|
+
// Trigger string agent harnesses match against to auto-load the skill.
|
|
3
|
+
// Kept terse and outcome-focused so it fires on "needs Jira" intents.
|
|
4
|
+
export const SKILL_DESCRIPTION = "Operate Jira through the acli-axi CLI - search work items with JQL, view full item detail, " +
|
|
5
|
+
"create and edit work items (including create-time custom fields like story points and sprint), " +
|
|
6
|
+
"and list board sprints. " +
|
|
7
|
+
"Use whenever a task touches Jira: searching or triaging work items, filing stories or bugs, " +
|
|
8
|
+
"editing summaries or assignees, or checking sprint contents.";
|
|
9
|
+
export const SKILL_AUTHOR = "Mathieu Neron";
|
|
10
|
+
function yamlDoubleQuote(value) {
|
|
11
|
+
return JSON.stringify(value);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Extract the `commands:` block from the top-level help so the skill's
|
|
15
|
+
* command list can never drift from what `acli-axi --help` prints.
|
|
16
|
+
*/
|
|
17
|
+
export function extractCommandsBlock() {
|
|
18
|
+
const match = TOP_LEVEL_HELP.match(/^(commands:\n(?: {2}.*\n)+)/m);
|
|
19
|
+
if (!match) {
|
|
20
|
+
throw new Error("Could not find commands block in TOP_LEVEL_HELP");
|
|
21
|
+
}
|
|
22
|
+
return match[1].trimEnd();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Render the installable SKILL.md for the acli-axi skill. The body is built
|
|
26
|
+
* from the same shared guidance the CLI prints (package description and
|
|
27
|
+
* top-level help). The package is published to npm, so the skill teaches the
|
|
28
|
+
* plain global install with npx as a one-off fallback.
|
|
29
|
+
*
|
|
30
|
+
* @returns full SKILL.md contents including YAML frontmatter
|
|
31
|
+
*/
|
|
32
|
+
export function createSkillMarkdown() {
|
|
33
|
+
return `---
|
|
34
|
+
name: acli-axi
|
|
35
|
+
description: ${yamlDoubleQuote(SKILL_DESCRIPTION)}
|
|
36
|
+
user-invocable: false
|
|
37
|
+
author: ${SKILL_AUTHOR}
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
# acli-axi
|
|
41
|
+
|
|
42
|
+
${DESCRIPTION}. Prefer it over raw \`acli\` for Jira reads and searches - output is TOON-encoded and token-efficient, with definitive totals and structured errors.
|
|
43
|
+
|
|
44
|
+
acli-axi is published to npm; install it globally:
|
|
45
|
+
|
|
46
|
+
\`\`\`sh
|
|
47
|
+
npm install -g acli-axi
|
|
48
|
+
\`\`\`
|
|
49
|
+
|
|
50
|
+
If \`acli-axi\` is not on PATH, ask the user to run that install (or offer to run it for them - it is a non-credential npm install), or use \`npx -y acli-axi\` for one-off runs.
|
|
51
|
+
|
|
52
|
+
acli-axi wraps the [Atlassian CLI (\`acli\`)](https://developer.atlassian.com/cloud/acli/), which must be installed and authenticated. If a command fails with an authentication error (\`AUTH_ERROR\`), ask the user to run \`acli jira auth login --web\` themselves - it is an interactive OAuth flow, never run it for them.
|
|
53
|
+
|
|
54
|
+
## When to use
|
|
55
|
+
|
|
56
|
+
Use acli-axi whenever a task touches Jira: searching work items with JQL, viewing one item's full detail, creating stories or bugs (including custom fields such as story points or sprint), editing summaries, assignees, labels, or types, or listing a board's sprints.
|
|
57
|
+
|
|
58
|
+
## Workflow
|
|
59
|
+
|
|
60
|
+
1. Run \`acli-axi\` with no arguments for a home view - auth state plus your open work items and suggested next commands.
|
|
61
|
+
2. Search with \`acli-axi search --jql "..."\`. Totals are definitive: when a page fills its \`--limit\`, the output reports \`total\` vs \`showing\` - raise \`--limit\` to see more instead of guessing.
|
|
62
|
+
3. Drill in with \`acli-axi view <KEY>\` (long descriptions truncate; add \`--full\` to expand).
|
|
63
|
+
4. Create with \`acli-axi create --project P --type T --summary S\`; custom fields go in as repeatable \`-f customfield_XXXXX=<value>\` flags. The response echoes the created item - no read-back needed.
|
|
64
|
+
5. Edit flag-editable fields with \`acli-axi edit <KEY> --summary ...\` (never prompts). Custom fields are create-only: acli's edit schema rejects them, and acli-axi refuses \`-f\` on edit with a structured error - that is an upstream acli limitation, not a bug to retry.
|
|
65
|
+
6. Every response ends with contextual next-step hints under \`help:\` - follow them.
|
|
66
|
+
|
|
67
|
+
## Commands
|
|
68
|
+
|
|
69
|
+
\`\`\`
|
|
70
|
+
${extractCommandsBlock()}
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
Run \`acli-axi --help\` for global flags, or \`acli-axi <command> --help\` for per-command usage.
|
|
74
|
+
|
|
75
|
+
The SDK built-in \`update\` command queries the npm registry, so it fails with a structured not-found while the package is unpublished - that is expected. Upgrade by re-running the git install command above.
|
|
76
|
+
|
|
77
|
+
## Tips
|
|
78
|
+
|
|
79
|
+
- Output is TOON-encoded and token-efficient; the gain scales with result count, so prefer \`search\` over per-item loops.
|
|
80
|
+
- Values passed to \`-f\` parse as JSON when possible (numbers, arrays); everything else stays a string.
|
|
81
|
+
- \`view\` can intermittently fail inside acli's JSON formatter and surfaces as a structured \`ACLI_ERROR\`; it is a read, so retrying once is safe.
|
|
82
|
+
- Errors are structured (\`error:\` + \`code:\` + \`help:\`): exit 2 for usage errors, exit 1 for everything else; stdout is data, stderr is diagnostics.
|
|
83
|
+
`;
|
|
84
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "acli-axi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AXI for Atlassian Jira \u2014 token-efficient agent interface over the Atlassian CLI (acli)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Mathieu Neron <mathieu.neron.ing@gmail.com>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/mathieu-neron/acli-axi.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/mathieu-neron/acli-axi#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/mathieu-neron/acli-axi/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"jira",
|
|
21
|
+
"atlassian",
|
|
22
|
+
"cli",
|
|
23
|
+
"agent",
|
|
24
|
+
"axi",
|
|
25
|
+
"toon",
|
|
26
|
+
"acli"
|
|
27
|
+
],
|
|
28
|
+
"bin": {
|
|
29
|
+
"acli-axi": "dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"skills/acli-axi",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"postbuild": "chmod +x dist/index.js",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"prepublishOnly": "npm run build",
|
|
45
|
+
"build:skill": "npm run build && node dist/skill-build.js"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"axi-sdk-js": "^0.1.8"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^20.19.0",
|
|
52
|
+
"typescript": "^5.8.0",
|
|
53
|
+
"vitest": "^3.2.0",
|
|
54
|
+
"yaml": "^2.9.0"
|
|
55
|
+
},
|
|
56
|
+
"packageManager": "pnpm@10.30.2"
|
|
57
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: acli-axi
|
|
3
|
+
description: "Operate Jira through the acli-axi CLI - search work items with JQL, view full item detail, create and edit work items (including create-time custom fields like story points and sprint), and list board sprints. Use whenever a task touches Jira: searching or triaging work items, filing stories or bugs, editing summaries or assignees, or checking sprint contents."
|
|
4
|
+
user-invocable: false
|
|
5
|
+
author: Mathieu Neron
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# acli-axi
|
|
9
|
+
|
|
10
|
+
AXI for Atlassian Jira — token-efficient agent interface over the Atlassian CLI (acli). Prefer it over raw `acli` for Jira reads and searches - output is TOON-encoded and token-efficient, with definitive totals and structured errors.
|
|
11
|
+
|
|
12
|
+
acli-axi is published to npm; install it globally:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install -g acli-axi
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If `acli-axi` is not on PATH, ask the user to run that install (or offer to run it for them - it is a non-credential npm install), or use `npx -y acli-axi` for one-off runs.
|
|
19
|
+
|
|
20
|
+
acli-axi wraps the [Atlassian CLI (`acli`)](https://developer.atlassian.com/cloud/acli/), which must be installed and authenticated. If a command fails with an authentication error (`AUTH_ERROR`), ask the user to run `acli jira auth login --web` themselves - it is an interactive OAuth flow, never run it for them.
|
|
21
|
+
|
|
22
|
+
## When to use
|
|
23
|
+
|
|
24
|
+
Use acli-axi whenever a task touches Jira: searching work items with JQL, viewing one item's full detail, creating stories or bugs (including custom fields such as story points or sprint), editing summaries, assignees, labels, or types, or listing a board's sprints.
|
|
25
|
+
|
|
26
|
+
## Workflow
|
|
27
|
+
|
|
28
|
+
1. Run `acli-axi` with no arguments for a home view - auth state plus your open work items and suggested next commands.
|
|
29
|
+
2. Search with `acli-axi search --jql "..."`. Totals are definitive: when a page fills its `--limit`, the output reports `total` vs `showing` - raise `--limit` to see more instead of guessing.
|
|
30
|
+
3. Drill in with `acli-axi view <KEY>` (long descriptions truncate; add `--full` to expand).
|
|
31
|
+
4. Create with `acli-axi create --project P --type T --summary S`; custom fields go in as repeatable `-f customfield_XXXXX=<value>` flags. The response echoes the created item - no read-back needed.
|
|
32
|
+
5. Edit flag-editable fields with `acli-axi edit <KEY> --summary ...` (never prompts). Custom fields are create-only: acli's edit schema rejects them, and acli-axi refuses `-f` on edit with a structured error - that is an upstream acli limitation, not a bug to retry.
|
|
33
|
+
6. Every response ends with contextual next-step hints under `help:` - follow them.
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
commands:
|
|
39
|
+
search --jql <query> [--limit N] [--fields a,b] Search work items
|
|
40
|
+
view <KEY> [--full] [--fields a,b] View one work item
|
|
41
|
+
create --project P --type T --summary S [...] Create a work item
|
|
42
|
+
edit <KEY> --summary|--assignee|--labels|... S Edit a work item
|
|
43
|
+
sprints --board <id> [--state active,future,closed] List board sprints
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run `acli-axi --help` for global flags, or `acli-axi <command> --help` for per-command usage.
|
|
47
|
+
|
|
48
|
+
The SDK built-in `update` command queries the npm registry, so it fails with a structured not-found while the package is unpublished - that is expected. Upgrade by re-running the git install command above.
|
|
49
|
+
|
|
50
|
+
## Tips
|
|
51
|
+
|
|
52
|
+
- Output is TOON-encoded and token-efficient; the gain scales with result count, so prefer `search` over per-item loops.
|
|
53
|
+
- Values passed to `-f` parse as JSON when possible (numbers, arrays); everything else stays a string.
|
|
54
|
+
- `view` can intermittently fail inside acli's JSON formatter and surfaces as a structured `ACLI_ERROR`; it is a read, so retrying once is safe.
|
|
55
|
+
- Errors are structured (`error:` + `code:` + `help:`): exit 2 for usage errors, exit 1 for everything else; stdout is data, stderr is diagnostics.
|