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 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,6 @@
1
+ export function parseAuthStatus(status) {
2
+ return {
3
+ site: status.match(/Site:\s*(\S+)/)?.[1],
4
+ email: status.match(/Email:\s*(\S+)/)?.[1],
5
+ };
6
+ }
@@ -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
+ });
@@ -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.