@jam-mcp/server 1.0.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/adapters/cache/noop-cache.d.ts +7 -0
  4. package/dist/adapters/cache/noop-cache.js +12 -0
  5. package/dist/adapters/credentials/composite.d.ts +30 -0
  6. package/dist/adapters/credentials/composite.js +79 -0
  7. package/dist/adapters/credentials/process-env.d.ts +13 -0
  8. package/dist/adapters/credentials/process-env.js +17 -0
  9. package/dist/adapters/credentials/secret-store.d.ts +69 -0
  10. package/dist/adapters/credentials/secret-store.js +259 -0
  11. package/dist/adapters/credentials/windows-user-env.d.ts +20 -0
  12. package/dist/adapters/credentials/windows-user-env.js +51 -0
  13. package/dist/adapters/jira-cloud/adf-to-text.d.ts +12 -0
  14. package/dist/adapters/jira-cloud/adf-to-text.js +151 -0
  15. package/dist/adapters/jira-cloud/jira-client.d.ts +32 -0
  16. package/dist/adapters/jira-cloud/jira-client.js +137 -0
  17. package/dist/adapters/jira-cloud/jira-read.adapter.d.ts +13 -0
  18. package/dist/adapters/jira-cloud/jira-read.adapter.js +97 -0
  19. package/dist/adapters/jira-cloud/jira-write.adapter.d.ts +12 -0
  20. package/dist/adapters/jira-cloud/jira-write.adapter.js +17 -0
  21. package/dist/adapters/jira-cloud/mapper.d.ts +34 -0
  22. package/dist/adapters/jira-cloud/mapper.js +143 -0
  23. package/dist/adapters/telemetry/console-telemetry.d.ts +13 -0
  24. package/dist/adapters/telemetry/console-telemetry.js +31 -0
  25. package/dist/application/get-full-issue-context.d.ts +18 -0
  26. package/dist/application/get-full-issue-context.js +120 -0
  27. package/dist/application/get-issue-context.d.ts +19 -0
  28. package/dist/application/get-issue-context.js +79 -0
  29. package/dist/application/search-issues.d.ts +22 -0
  30. package/dist/application/search-issues.js +93 -0
  31. package/dist/bootstrap/boot-health-gate.d.ts +25 -0
  32. package/dist/bootstrap/boot-health-gate.js +137 -0
  33. package/dist/bootstrap/bootstrap-orchestrator.d.ts +15 -0
  34. package/dist/bootstrap/bootstrap-orchestrator.js +14 -0
  35. package/dist/bootstrap/host-mcp.d.ts +54 -0
  36. package/dist/bootstrap/host-mcp.js +98 -0
  37. package/dist/bootstrap/jira-projects.d.ts +16 -0
  38. package/dist/bootstrap/jira-projects.js +24 -0
  39. package/dist/bootstrap/mcp-config-merger.d.ts +58 -0
  40. package/dist/bootstrap/mcp-config-merger.js +119 -0
  41. package/dist/bootstrap/migration-target.d.ts +60 -0
  42. package/dist/bootstrap/migration-target.js +80 -0
  43. package/dist/bootstrap/project-bindings.d.ts +48 -0
  44. package/dist/bootstrap/project-bindings.js +112 -0
  45. package/dist/bootstrap/project-config-bootstrapper.d.ts +37 -0
  46. package/dist/bootstrap/project-config-bootstrapper.js +69 -0
  47. package/dist/bootstrap/project-config-resolver.d.ts +53 -0
  48. package/dist/bootstrap/project-config-resolver.js +59 -0
  49. package/dist/bootstrap/project-root-resolver.d.ts +22 -0
  50. package/dist/bootstrap/project-root-resolver.js +43 -0
  51. package/dist/bootstrap/setup-apply.d.ts +31 -0
  52. package/dist/bootstrap/setup-apply.js +61 -0
  53. package/dist/bootstrap/setup-plan.d.ts +102 -0
  54. package/dist/bootstrap/setup-plan.js +224 -0
  55. package/dist/bootstrap/setup-state.d.ts +73 -0
  56. package/dist/bootstrap/setup-state.js +89 -0
  57. package/dist/bootstrap/workspace-identity.d.ts +52 -0
  58. package/dist/bootstrap/workspace-identity.js +114 -0
  59. package/dist/cli/agent-api.d.ts +63 -0
  60. package/dist/cli/agent-api.js +149 -0
  61. package/dist/cli/auth.d.ts +39 -0
  62. package/dist/cli/auth.js +201 -0
  63. package/dist/cli/doctor.d.ts +7 -0
  64. package/dist/cli/doctor.js +31 -0
  65. package/dist/cli/runtime.d.ts +16 -0
  66. package/dist/cli/runtime.js +97 -0
  67. package/dist/cli/serve.d.ts +7 -0
  68. package/dist/cli/serve.js +23 -0
  69. package/dist/cli/setup-wizard.d.ts +37 -0
  70. package/dist/cli/setup-wizard.js +338 -0
  71. package/dist/cli/setup.d.ts +26 -0
  72. package/dist/cli/setup.js +214 -0
  73. package/dist/cli/ui.d.ts +118 -0
  74. package/dist/cli/ui.js +338 -0
  75. package/dist/cli-entry.d.ts +7 -0
  76. package/dist/cli-entry.js +124 -0
  77. package/dist/config/load-config.d.ts +18 -0
  78. package/dist/config/load-config.js +48 -0
  79. package/dist/config/schema.d.ts +36 -0
  80. package/dist/config/schema.js +74 -0
  81. package/dist/deps.d.ts +50 -0
  82. package/dist/deps.js +39 -0
  83. package/dist/domain/completeness.d.ts +63 -0
  84. package/dist/domain/completeness.js +20 -0
  85. package/dist/domain/context.d.ts +38 -0
  86. package/dist/domain/context.js +1 -0
  87. package/dist/domain/errors.d.ts +27 -0
  88. package/dist/domain/errors.js +49 -0
  89. package/dist/domain/issue.d.ts +35 -0
  90. package/dist/domain/issue.js +1 -0
  91. package/dist/index.d.ts +2 -0
  92. package/dist/index.js +17 -0
  93. package/dist/mcp/create-server.d.ts +9 -0
  94. package/dist/mcp/create-server.js +32 -0
  95. package/dist/mcp/tool-result.d.ts +10 -0
  96. package/dist/mcp/tool-result.js +33 -0
  97. package/dist/mcp/tools/jira-context.tool.d.ts +3 -0
  98. package/dist/mcp/tools/jira-context.tool.js +25 -0
  99. package/dist/mcp/tools/jira-full.tool.d.ts +3 -0
  100. package/dist/mcp/tools/jira-full.tool.js +27 -0
  101. package/dist/mcp/tools/jira-search.tool.d.ts +3 -0
  102. package/dist/mcp/tools/jira-search.tool.js +31 -0
  103. package/dist/policy/completeness-policy.d.ts +19 -0
  104. package/dist/policy/completeness-policy.js +34 -0
  105. package/dist/policy/consistency-policy.d.ts +13 -0
  106. package/dist/policy/consistency-policy.js +6 -0
  107. package/dist/policy/field-policy.d.ts +11 -0
  108. package/dist/policy/field-policy.js +26 -0
  109. package/dist/policy/output-budget-policy.d.ts +23 -0
  110. package/dist/policy/output-budget-policy.js +96 -0
  111. package/dist/policy/pagination-policy.d.ts +15 -0
  112. package/dist/policy/pagination-policy.js +13 -0
  113. package/dist/ports/cache.port.d.ts +5 -0
  114. package/dist/ports/cache.port.js +1 -0
  115. package/dist/ports/credentials.port.d.ts +27 -0
  116. package/dist/ports/credentials.port.js +1 -0
  117. package/dist/ports/jira-read.port.d.ts +73 -0
  118. package/dist/ports/jira-read.port.js +1 -0
  119. package/dist/ports/jira-write.port.d.ts +12 -0
  120. package/dist/ports/jira-write.port.js +1 -0
  121. package/dist/ports/telemetry.port.d.ts +13 -0
  122. package/dist/ports/telemetry.port.js +1 -0
  123. package/package.json +69 -0
@@ -0,0 +1,118 @@
1
+ /**
2
+ * The whole of JAM's terminal presentation.
3
+ *
4
+ * Deliberately hand-rolled rather than pulled from a TUI library: JAM needs a
5
+ * status line, a section heading and a single-choice list, and a dependency
6
+ * that renders full-screen frames would be a lot of surface for that. It also
7
+ * keeps the non-TTY path honest - everything here degrades to plain lines.
8
+ */
9
+ export type UiOptions = {
10
+ stream?: NodeJS.WriteStream;
11
+ input?: NodeJS.ReadStream;
12
+ /** Force interactivity on or off; defaults to detection. */
13
+ interactive?: boolean;
14
+ color?: boolean;
15
+ };
16
+ export declare const SYMBOLS: {
17
+ readonly section: "◆";
18
+ readonly success: "✓";
19
+ readonly warning: "!";
20
+ readonly failure: "×";
21
+ readonly selected: "❯";
22
+ readonly pending: "○";
23
+ readonly next: "›";
24
+ };
25
+ /**
26
+ * Colour is suppressed for NO_COLOR, for CI, and for anything that is not a
27
+ * TTY - a log file full of escape codes helps nobody. Meaning is never carried
28
+ * by colour alone; every state also has a distinct symbol.
29
+ */
30
+ export declare function colorEnabled(stream: NodeJS.WriteStream, env?: NodeJS.ProcessEnv): boolean;
31
+ export declare function interactiveEnabled(stream: NodeJS.WriteStream, input: NodeJS.ReadStream, env?: NodeJS.ProcessEnv): boolean;
32
+ export declare class CancelledError extends Error {
33
+ constructor();
34
+ }
35
+ /**
36
+ * Raised when a question is unavoidable but there is no terminal to ask it in.
37
+ * Distinct from any JAM/Jira error: nothing is wrong with the setup, there is
38
+ * just no one to answer, and the fix is a flag rather than a diagnosis.
39
+ */
40
+ export declare class NonInteractiveError extends Error {
41
+ /** A whole instruction, printed as given - not a bare flag or command. */
42
+ readonly flagHint: string;
43
+ constructor(question: string, flagHint: string);
44
+ }
45
+ /**
46
+ * The one mapping of a prompt that could not run to an exit code.
47
+ *
48
+ * Returns undefined for anything else, which the caller must rethrow: a
49
+ * cancelled prompt is not a failure, and a missing terminal is guidance rather
50
+ * than a diagnosis, but a real fault must still surface as one.
51
+ */
52
+ export declare function reportPromptError(err: unknown, ui: Ui): number | undefined;
53
+ export type Choice<T> = {
54
+ value: T;
55
+ label: string;
56
+ /** Shown dimmed under the label. */
57
+ hint?: string;
58
+ };
59
+ export declare class Ui {
60
+ readonly interactive: boolean;
61
+ private readonly stream;
62
+ private readonly input;
63
+ private readonly useColor;
64
+ constructor(options?: UiOptions);
65
+ private paint;
66
+ write(text: string): void;
67
+ line(text?: string): void;
68
+ /**
69
+ * A stage heading. Sections replace a rigid "[2/5]" counter, which lies
70
+ * whenever a step turns out to be already done and gets skipped.
71
+ */
72
+ section(title: string, subtitle?: string): void;
73
+ success(text: string, detail?: string): void;
74
+ warn(text: string, detail?: string): void;
75
+ failure(text: string, detail?: string): void;
76
+ pending(text: string, detail?: string): void;
77
+ next(text: string): void;
78
+ private status;
79
+ /**
80
+ * Spinner for genuine waiting only - a network round trip, a package
81
+ * download. Spinning while reading a local file is theatre that makes fast
82
+ * operations look slow.
83
+ */
84
+ spin<T>(label: string, work: () => Promise<T>): Promise<T>;
85
+ /**
86
+ * Free-text answer, echoed as it is typed.
87
+ *
88
+ * The line editor is Node's readline, not JAM's: IME composition, CJK width,
89
+ * grapheme deletion, cursor movement and paste are the terminal's job, and a
90
+ * hand-rolled reader gets every one of them wrong. Cancelling is Ctrl-C,
91
+ * Ctrl-D or a closed stdin - readline swallows a lone Escape, so unlike
92
+ * `secret()` and `select()` this prompt cannot be cancelled with it.
93
+ */
94
+ prompt(question: string, flagHint: string, fallback?: string): Promise<string>;
95
+ /**
96
+ * Secret answer - nothing typed is ever echoed, to this stream or any other.
97
+ *
98
+ * No dots, no length hint, no redraw: an API token pasted into a terminal
99
+ * should leave no trace on screen for someone glancing over, in a screen
100
+ * share, or in scrollback.
101
+ */
102
+ secret(question: string, flagHint: string): Promise<string>;
103
+ /**
104
+ * The only place raw mode and a hand-managed buffer still live.
105
+ *
106
+ * A token needs four things - no echo, paste, delete, Enter - and a line
107
+ * editor that echoes nothing is not something readline offers. Everything
108
+ * `prompt` needs and this does not - cursor movement, IME, character width -
109
+ * stays out.
110
+ */
111
+ private readSecret;
112
+ /**
113
+ * Single-choice list. Throws in non-interactive mode rather than silently
114
+ * picking a default - an unattended run that quietly chose for you is worse
115
+ * than one that tells you which flag to pass.
116
+ */
117
+ select<T>(question: string, choices: Choice<T>[], flagHint: string): Promise<T>;
118
+ }
package/dist/cli/ui.js ADDED
@@ -0,0 +1,338 @@
1
+ import { createInterface, emitKeypressEvents } from "node:readline";
2
+ export const SYMBOLS = {
3
+ section: "◆",
4
+ success: "✓",
5
+ warning: "!",
6
+ failure: "×",
7
+ selected: "❯",
8
+ pending: "○",
9
+ next: "›",
10
+ };
11
+ // Escape sequences are spelled out rather than typed literally: a raw 0x1b in
12
+ // source is invisible in most editors and does not survive careless tooling.
13
+ const CSI = "[";
14
+ const ANSI = {
15
+ reset: `${CSI}0m`,
16
+ dim: `${CSI}2m`,
17
+ cyan: `${CSI}36m`,
18
+ green: `${CSI}32m`,
19
+ yellow: `${CSI}33m`,
20
+ red: `${CSI}31m`,
21
+ hideCursor: `${CSI}?25l`,
22
+ showCursor: `${CSI}?25h`,
23
+ };
24
+ const cursorUp = (rows) => `${CSI}${rows}A`;
25
+ /**
26
+ * Colour is suppressed for NO_COLOR, for CI, and for anything that is not a
27
+ * TTY - a log file full of escape codes helps nobody. Meaning is never carried
28
+ * by colour alone; every state also has a distinct symbol.
29
+ */
30
+ export function colorEnabled(stream, env = process.env) {
31
+ if (env["NO_COLOR"] !== undefined)
32
+ return false;
33
+ if (env["CI"])
34
+ return false;
35
+ return Boolean(stream.isTTY);
36
+ }
37
+ export function interactiveEnabled(stream, input, env = process.env) {
38
+ if (env["CI"])
39
+ return false;
40
+ return Boolean(stream.isTTY && input.isTTY);
41
+ }
42
+ export class CancelledError extends Error {
43
+ constructor() {
44
+ super("Cancelled.");
45
+ this.name = "CancelledError";
46
+ }
47
+ }
48
+ /**
49
+ * Raised when a question is unavoidable but there is no terminal to ask it in.
50
+ * Distinct from any JAM/Jira error: nothing is wrong with the setup, there is
51
+ * just no one to answer, and the fix is a flag rather than a diagnosis.
52
+ */
53
+ export class NonInteractiveError extends Error {
54
+ /** A whole instruction, printed as given - not a bare flag or command. */
55
+ flagHint;
56
+ constructor(question, flagHint) {
57
+ super(`${question} cannot be asked without a terminal.`);
58
+ this.name = "NonInteractiveError";
59
+ this.flagHint = flagHint;
60
+ }
61
+ }
62
+ /**
63
+ * The one mapping of a prompt that could not run to an exit code.
64
+ *
65
+ * Returns undefined for anything else, which the caller must rethrow: a
66
+ * cancelled prompt is not a failure, and a missing terminal is guidance rather
67
+ * than a diagnosis, but a real fault must still surface as one.
68
+ */
69
+ export function reportPromptError(err, ui) {
70
+ if (err instanceof CancelledError) {
71
+ ui.line();
72
+ ui.warn("Cancelled. Nothing was changed.");
73
+ return 130;
74
+ }
75
+ if (err instanceof NonInteractiveError) {
76
+ ui.line();
77
+ ui.failure(err.message);
78
+ // Printed verbatim. A hint that is a sentence must not be dressed up as a
79
+ // command the user could paste, and JAM cannot know their shell anyway.
80
+ ui.next(err.flagHint);
81
+ return 1;
82
+ }
83
+ return undefined;
84
+ }
85
+ export class Ui {
86
+ interactive;
87
+ stream;
88
+ input;
89
+ useColor;
90
+ constructor(options = {}) {
91
+ this.stream = options.stream ?? process.stdout;
92
+ this.input = options.input ?? process.stdin;
93
+ this.useColor = options.color ?? colorEnabled(this.stream);
94
+ this.interactive = options.interactive ?? interactiveEnabled(this.stream, this.input);
95
+ }
96
+ paint(text, code) {
97
+ return this.useColor ? `${ANSI[code]}${text}${ANSI.reset}` : text;
98
+ }
99
+ write(text) {
100
+ this.stream.write(text);
101
+ }
102
+ line(text = "") {
103
+ this.write(`${text}\n`);
104
+ }
105
+ /**
106
+ * A stage heading. Sections replace a rigid "[2/5]" counter, which lies
107
+ * whenever a step turns out to be already done and gets skipped.
108
+ */
109
+ section(title, subtitle) {
110
+ this.line();
111
+ this.line(`${this.paint(SYMBOLS.section, "cyan")} ${title}`);
112
+ if (subtitle)
113
+ this.line(` ${this.paint(subtitle, "dim")}`);
114
+ }
115
+ success(text, detail) {
116
+ this.status(this.paint(SYMBOLS.success, "green"), text, detail);
117
+ }
118
+ warn(text, detail) {
119
+ this.status(this.paint(SYMBOLS.warning, "yellow"), text, detail);
120
+ }
121
+ failure(text, detail) {
122
+ this.status(this.paint(SYMBOLS.failure, "red"), text, detail);
123
+ }
124
+ pending(text, detail) {
125
+ this.status(this.paint(SYMBOLS.pending, "dim"), text, detail);
126
+ }
127
+ next(text) {
128
+ this.line();
129
+ this.line(`${this.paint(SYMBOLS.next, "cyan")} ${text}`);
130
+ }
131
+ status(symbol, text, detail) {
132
+ const padded = detail ? text.padEnd(22) : text;
133
+ this.line(`${symbol} ${padded}${detail ? ` ${this.paint(detail, "dim")}` : ""}`);
134
+ }
135
+ /**
136
+ * Spinner for genuine waiting only - a network round trip, a package
137
+ * download. Spinning while reading a local file is theatre that makes fast
138
+ * operations look slow.
139
+ */
140
+ async spin(label, work) {
141
+ if (!this.interactive) {
142
+ this.pending(label);
143
+ return work();
144
+ }
145
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
146
+ let i = 0;
147
+ this.write(ANSI.hideCursor);
148
+ const timer = setInterval(() => {
149
+ this.write(`\r${this.paint(frames[i % frames.length], "cyan")} ${label}`);
150
+ i++;
151
+ }, 80);
152
+ try {
153
+ return await work();
154
+ }
155
+ finally {
156
+ clearInterval(timer);
157
+ this.write(`\r${" ".repeat(label.length + 4)}\r`);
158
+ this.write(ANSI.showCursor);
159
+ }
160
+ }
161
+ /**
162
+ * Free-text answer, echoed as it is typed.
163
+ *
164
+ * The line editor is Node's readline, not JAM's: IME composition, CJK width,
165
+ * grapheme deletion, cursor movement and paste are the terminal's job, and a
166
+ * hand-rolled reader gets every one of them wrong. Cancelling is Ctrl-C,
167
+ * Ctrl-D or a closed stdin - readline swallows a lone Escape, so unlike
168
+ * `secret()` and `select()` this prompt cannot be cancelled with it.
169
+ */
170
+ async prompt(question, flagHint, fallback) {
171
+ if (!this.interactive)
172
+ throw new NonInteractiveError(question, flagHint);
173
+ const suffix = fallback ? ` ${this.paint(`[${fallback}]`, "dim")}` : "";
174
+ // One interface per question, closed in finally. A long-lived one on
175
+ // process.stdin would swallow the MCP stdio transport in `jam serve`.
176
+ // historySize 0: a Jira URL and an email are the user's, not JAM's to keep.
177
+ const rl = createInterface({
178
+ input: this.input,
179
+ output: this.stream,
180
+ terminal: true,
181
+ historySize: 0,
182
+ });
183
+ try {
184
+ const answer = await new Promise((resolve, reject) => {
185
+ // Ctrl-C, Ctrl-D and a stdin that ends all arrive as `close`. Without
186
+ // this the question would hang on a stream nobody is answering.
187
+ rl.once("close", () => reject(new CancelledError()));
188
+ rl.question(`${question}${suffix} `, resolve);
189
+ });
190
+ return answer || (fallback ?? "");
191
+ }
192
+ finally {
193
+ rl.close();
194
+ }
195
+ }
196
+ /**
197
+ * Secret answer - nothing typed is ever echoed, to this stream or any other.
198
+ *
199
+ * No dots, no length hint, no redraw: an API token pasted into a terminal
200
+ * should leave no trace on screen for someone glancing over, in a screen
201
+ * share, or in scrollback.
202
+ */
203
+ async secret(question, flagHint) {
204
+ if (!this.interactive)
205
+ throw new NonInteractiveError(question, flagHint);
206
+ return this.readSecret(question);
207
+ }
208
+ /**
209
+ * The only place raw mode and a hand-managed buffer still live.
210
+ *
211
+ * A token needs four things - no echo, paste, delete, Enter - and a line
212
+ * editor that echoes nothing is not something readline offers. Everything
213
+ * `prompt` needs and this does not - cursor movement, IME, character width -
214
+ * stays out.
215
+ */
216
+ async readSecret(question) {
217
+ this.write(`${question} `);
218
+ emitKeypressEvents(this.input);
219
+ const wasRaw = this.input.isRaw ?? false;
220
+ this.input.setRawMode?.(true);
221
+ this.input.resume();
222
+ let buffer = "";
223
+ let removeListener = () => { };
224
+ try {
225
+ return await new Promise((resolve, reject) => {
226
+ const onKey = (str, key) => {
227
+ if (key.name === "return" || key.name === "enter") {
228
+ this.line();
229
+ resolve(buffer);
230
+ return;
231
+ }
232
+ if (key.name === "escape" || (key.ctrl && key.name === "c")) {
233
+ this.line();
234
+ reject(new CancelledError());
235
+ return;
236
+ }
237
+ // Every shape a delete arrives in. Terminals disagree: Backspace may
238
+ // send DEL (0x7f) or BS (0x08), and a Delete key can arrive as an
239
+ // escape sequence that readline names "delete". Matching only
240
+ // key.name === "backspace" leaves the rest to fall through to the
241
+ // printable branch, where they are stripped to "" and silently do
242
+ // nothing - which reads as "backspace is broken".
243
+ if (key.name === "backspace" ||
244
+ key.name === "delete" ||
245
+ str === "\u007f" ||
246
+ str === "\b") {
247
+ // Drops one UTF-16 unit, not one grapheme. An API token is ASCII,
248
+ // and no Unicode editor is coming here - that is what `prompt` and
249
+ // readline are for.
250
+ if (buffer.length === 0)
251
+ return;
252
+ buffer = buffer.slice(0, -1);
253
+ return;
254
+ }
255
+ // Ignore control keys; take printable characters, including whole
256
+ // runs of them, which is how a paste arrives.
257
+ if (key.ctrl || str === undefined || str === "")
258
+ return;
259
+ const printable = str.replace(/[\u0000-\u001f\u007f]/g, "");
260
+ if (!printable)
261
+ return;
262
+ buffer += printable;
263
+ };
264
+ removeListener = () => this.input.off("keypress", onKey);
265
+ this.input.on("keypress", onKey);
266
+ });
267
+ }
268
+ finally {
269
+ // Teardown lives here and nowhere else: a listener stranded by a throw
270
+ // would let the next prompt receive the tail of a token. No cursor
271
+ // restore - this never hides it, and a no-op write would only suggest
272
+ // otherwise.
273
+ removeListener();
274
+ this.input.setRawMode?.(wasRaw);
275
+ this.input.pause();
276
+ }
277
+ }
278
+ /**
279
+ * Single-choice list. Throws in non-interactive mode rather than silently
280
+ * picking a default - an unattended run that quietly chose for you is worse
281
+ * than one that tells you which flag to pass.
282
+ */
283
+ async select(question, choices, flagHint) {
284
+ if (choices.length === 0)
285
+ throw new Error("select() needs at least one choice");
286
+ if (!this.interactive)
287
+ throw new NonInteractiveError(question, flagHint);
288
+ let index = 0;
289
+ const rows = choices.length * 2 + 1;
290
+ const render = (first) => {
291
+ if (!first)
292
+ this.write(cursorUp(rows));
293
+ this.line(question);
294
+ choices.forEach((choice, i) => {
295
+ const active = i === index;
296
+ const marker = active ? this.paint(SYMBOLS.selected, "cyan") : " ";
297
+ const label = active ? this.paint(choice.label, "cyan") : choice.label;
298
+ this.line(`${marker} ${label}`);
299
+ this.line(` ${this.paint(choice.hint ?? "", "dim")}`);
300
+ });
301
+ };
302
+ render(true);
303
+ this.write(ANSI.hideCursor);
304
+ emitKeypressEvents(this.input);
305
+ const wasRaw = this.input.isRaw ?? false;
306
+ this.input.setRawMode?.(true);
307
+ this.input.resume();
308
+ let removeListener = () => { };
309
+ try {
310
+ return await new Promise((resolve, reject) => {
311
+ const onKey = (_str, key) => {
312
+ if (key.name === "up") {
313
+ index = (index - 1 + choices.length) % choices.length;
314
+ render(false);
315
+ }
316
+ else if (key.name === "down") {
317
+ index = (index + 1) % choices.length;
318
+ render(false);
319
+ }
320
+ else if (key.name === "return") {
321
+ resolve(choices[index].value);
322
+ }
323
+ else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
324
+ reject(new CancelledError());
325
+ }
326
+ };
327
+ removeListener = () => this.input.off("keypress", onKey);
328
+ this.input.on("keypress", onKey);
329
+ });
330
+ }
331
+ finally {
332
+ removeListener();
333
+ this.input.setRawMode?.(wasRaw);
334
+ this.input.pause();
335
+ this.write(ANSI.showCursor);
336
+ }
337
+ }
338
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Command dispatch for the JAM CLI, separated from the bin so other entry
3
+ * points (notably @jam-mcp/bootstrap) can forward to exactly these commands
4
+ * instead of reimplementing them.
5
+ */
6
+ export declare const USAGE = "jam - Jira Agent MCP\n\nUsage:\n jam serve Run the MCP server over stdio (default; this is what Claude Code / Codex launch)\n jam doctor Diagnose config, credentials and Jira connectivity\n jam setup [--project KEY] [--shared] [--migrate] [--non-interactive]\n Wire up this project and run doctor. Binds it to you\n alone, writing nothing to the repository; --shared\n adopts JAM for the team (project.yaml, .mcp.json)\n jam runtime Show which JAM build this machine runs\n jam runtime use package | development <path>\n Change it (writes ~/.jam/config.yaml only, never a project)\n jam auth login Store Jira credentials in this user's OS secret store\n jam auth logout Remove them again\n\nFor coding agents and scripts (stdout is JSON only, never prompts):\n jam setup --agent One shot: detect, plan, apply what is safe, verify\n jam setup plan --json Report what setup would change, changing nothing\n jam setup apply --non-interactive --json\n Execute the plan\n jam doctor --json Health check as structured output\n jam auth status --json Whether Jira credentials are configured (never their value)\n\nEnvironment:\n JIRA_BASE_URL https://your-site.atlassian.net\n JIRA_EMAIL Atlassian account email\n JIRA_API_TOKEN Atlassian API token\n JAM_PROJECT_KEY Jira project key, used by `jam setup`/`jam serve` when no\n .jira-agent/project.yaml exists yet\n\nCredentials and JAM_PROJECT_KEY are read from the current shell's environment\nfirst, then (on Windows) from the User environment - so a value set with\n`setx` works without opening a new terminal.\n";
7
+ export declare function runJamCommand(argv: string[]): Promise<number>;
@@ -0,0 +1,124 @@
1
+ import { authLoginCommand, authLogoutCommand } from "./cli/auth.js";
2
+ import { doctor } from "./cli/doctor.js";
3
+ import { showRuntime, useRuntime } from "./cli/runtime.js";
4
+ import { serve } from "./cli/serve.js";
5
+ import { setup } from "./cli/setup.js";
6
+ import { runSetupWizard } from "./cli/setup-wizard.js";
7
+ import { reportPromptError, Ui } from "./cli/ui.js";
8
+ import { authStatusCommand, doctorJsonCommand, setupAgentCommand, setupApplyCommand, setupPlanCommand, } from "./cli/agent-api.js";
9
+ /**
10
+ * Command dispatch for the JAM CLI, separated from the bin so other entry
11
+ * points (notably @jam-mcp/bootstrap) can forward to exactly these commands
12
+ * instead of reimplementing them.
13
+ */
14
+ export const USAGE = `jam - Jira Agent MCP
15
+
16
+ Usage:
17
+ jam serve Run the MCP server over stdio (default; this is what Claude Code / Codex launch)
18
+ jam doctor Diagnose config, credentials and Jira connectivity
19
+ jam setup [--project KEY] [--shared] [--migrate] [--non-interactive]
20
+ Wire up this project and run doctor. Binds it to you
21
+ alone, writing nothing to the repository; --shared
22
+ adopts JAM for the team (project.yaml, .mcp.json)
23
+ jam runtime Show which JAM build this machine runs
24
+ jam runtime use package | development <path>
25
+ Change it (writes ~/.jam/config.yaml only, never a project)
26
+ jam auth login Store Jira credentials in this user's OS secret store
27
+ jam auth logout Remove them again
28
+
29
+ For coding agents and scripts (stdout is JSON only, never prompts):
30
+ jam setup --agent One shot: detect, plan, apply what is safe, verify
31
+ jam setup plan --json Report what setup would change, changing nothing
32
+ jam setup apply --non-interactive --json
33
+ Execute the plan
34
+ jam doctor --json Health check as structured output
35
+ jam auth status --json Whether Jira credentials are configured (never their value)
36
+
37
+ Environment:
38
+ JIRA_BASE_URL https://your-site.atlassian.net
39
+ JIRA_EMAIL Atlassian account email
40
+ JIRA_API_TOKEN Atlassian API token
41
+ JAM_PROJECT_KEY Jira project key, used by \`jam setup\`/\`jam serve\` when no
42
+ .jira-agent/project.yaml exists yet
43
+
44
+ Credentials and JAM_PROJECT_KEY are read from the current shell's environment
45
+ first, then (on Windows) from the User environment - so a value set with
46
+ \`setx\` works without opening a new terminal.
47
+ `;
48
+ function findFlagValue(argv, flag) {
49
+ const index = argv.indexOf(flag);
50
+ return index >= 0 ? argv[index + 1] : undefined;
51
+ }
52
+ /**
53
+ * Commands that may ask a question share the wizard's mapping: a cancelled
54
+ * prompt is not a failure, and a missing terminal is guidance rather than a
55
+ * diagnosis.
56
+ */
57
+ async function withPrompts(run) {
58
+ const ui = new Ui();
59
+ try {
60
+ return await run();
61
+ }
62
+ catch (err) {
63
+ const code = reportPromptError(err, ui);
64
+ if (code === undefined)
65
+ throw err;
66
+ return code;
67
+ }
68
+ }
69
+ export async function runJamCommand(argv) {
70
+ const [command, ...rest] = argv;
71
+ switch (command ?? "serve") {
72
+ case "serve":
73
+ return serve();
74
+ case "doctor":
75
+ return rest.includes("--json") ? doctorJsonCommand() : doctor();
76
+ case "setup": {
77
+ const explicitKey = findFlagValue(rest, "--project");
78
+ const common = {
79
+ ...(explicitKey ? { explicitKey } : {}),
80
+ ...(rest.includes("--migrate") ? { migrate: true } : {}),
81
+ // Personal by default: JAM is configured for this user, and the
82
+ // repository is only written to when someone asks for the team.
83
+ ...(rest.includes("--shared") ? { shared: true } : {}),
84
+ };
85
+ // Agent entry points first: each is non-interactive by construction, so
86
+ // no question can ever block an automated caller.
87
+ if (rest.includes("--agent"))
88
+ return setupAgentCommand(common);
89
+ if (rest[0] === "plan")
90
+ return setupPlanCommand(common);
91
+ if (rest[0] === "apply")
92
+ return setupApplyCommand(common);
93
+ // The wizard can ask; the plain path never does.
94
+ return rest.includes("--non-interactive") ? setup(common) : runSetupWizard(common);
95
+ }
96
+ case "runtime": {
97
+ const json = rest.includes("--json");
98
+ if (rest[0] === "use")
99
+ return useRuntime(rest[1], rest[2], { json });
100
+ return showRuntime({ json });
101
+ }
102
+ case "auth": {
103
+ if (rest[0] === "status")
104
+ return authStatusCommand();
105
+ // login/logout are human-only by design: an agent must stop at
106
+ // JAM_AUTH_REQUIRED and hand the person this command, never run it with a
107
+ // token it was given.
108
+ if (rest[0] === "login")
109
+ return withPrompts(() => authLoginCommand());
110
+ if (rest[0] === "logout")
111
+ return withPrompts(async () => authLogoutCommand());
112
+ process.stderr.write("Usage: jam auth login | status [--json] | logout\n");
113
+ return 1;
114
+ }
115
+ case "help":
116
+ case "--help":
117
+ case "-h":
118
+ process.stdout.write(USAGE);
119
+ return 0;
120
+ default:
121
+ process.stderr.write(`Unknown command: ${command}\n\n${USAGE}`);
122
+ return 1;
123
+ }
124
+ }
@@ -0,0 +1,18 @@
1
+ import { type ProjectConfig } from "./schema.js";
2
+ export declare const CONFIG_RELATIVE_PATH: string;
3
+ /**
4
+ * Walk up from `startDir` looking for `.jira-agent/project.yaml`.
5
+ * Returns undefined when the project has no JAM config at all.
6
+ */
7
+ export declare function findConfigPath(startDir?: string): string | undefined;
8
+ export type LoadedConfig = {
9
+ config: ProjectConfig;
10
+ /** Undefined when defaults were used because no file was found. */
11
+ path?: string;
12
+ };
13
+ /**
14
+ * A missing config file is not fatal - JAM falls back to schema defaults so a
15
+ * bare `jam serve` still works. A malformed one IS fatal: silently running
16
+ * with the wrong field policy is worse than refusing to start.
17
+ */
18
+ export declare function loadConfig(startDir?: string): LoadedConfig;
@@ -0,0 +1,48 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { JamError } from "../domain/errors.js";
5
+ import { ProjectConfigSchema } from "./schema.js";
6
+ export const CONFIG_RELATIVE_PATH = join(".jira-agent", "project.yaml");
7
+ /**
8
+ * Walk up from `startDir` looking for `.jira-agent/project.yaml`.
9
+ * Returns undefined when the project has no JAM config at all.
10
+ */
11
+ export function findConfigPath(startDir = process.cwd()) {
12
+ let dir = resolve(startDir);
13
+ for (;;) {
14
+ const candidate = join(dir, CONFIG_RELATIVE_PATH);
15
+ if (existsSync(candidate))
16
+ return candidate;
17
+ const parent = dirname(dir);
18
+ if (parent === dir)
19
+ return undefined;
20
+ dir = parent;
21
+ }
22
+ }
23
+ /**
24
+ * A missing config file is not fatal - JAM falls back to schema defaults so a
25
+ * bare `jam serve` still works. A malformed one IS fatal: silently running
26
+ * with the wrong field policy is worse than refusing to start.
27
+ */
28
+ export function loadConfig(startDir = process.cwd()) {
29
+ const path = findConfigPath(startDir);
30
+ if (!path) {
31
+ return { config: ProjectConfigSchema.parse({}) };
32
+ }
33
+ let raw;
34
+ try {
35
+ raw = parseYaml(readFileSync(path, "utf8"));
36
+ }
37
+ catch (err) {
38
+ throw new JamError("CONFIG_INVALID", `Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`, { path });
39
+ }
40
+ const parsed = ProjectConfigSchema.safeParse(raw ?? {});
41
+ if (!parsed.success) {
42
+ const issues = parsed.error.issues
43
+ .map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
44
+ .join("; ");
45
+ throw new JamError("CONFIG_INVALID", `Invalid ${path} - ${issues}`, { path });
46
+ }
47
+ return { config: parsed.data, path };
48
+ }