@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.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/adapters/cache/noop-cache.d.ts +7 -0
- package/dist/adapters/cache/noop-cache.js +12 -0
- package/dist/adapters/credentials/composite.d.ts +30 -0
- package/dist/adapters/credentials/composite.js +79 -0
- package/dist/adapters/credentials/process-env.d.ts +13 -0
- package/dist/adapters/credentials/process-env.js +17 -0
- package/dist/adapters/credentials/secret-store.d.ts +69 -0
- package/dist/adapters/credentials/secret-store.js +259 -0
- package/dist/adapters/credentials/windows-user-env.d.ts +20 -0
- package/dist/adapters/credentials/windows-user-env.js +51 -0
- package/dist/adapters/jira-cloud/adf-to-text.d.ts +12 -0
- package/dist/adapters/jira-cloud/adf-to-text.js +151 -0
- package/dist/adapters/jira-cloud/jira-client.d.ts +32 -0
- package/dist/adapters/jira-cloud/jira-client.js +137 -0
- package/dist/adapters/jira-cloud/jira-read.adapter.d.ts +13 -0
- package/dist/adapters/jira-cloud/jira-read.adapter.js +97 -0
- package/dist/adapters/jira-cloud/jira-write.adapter.d.ts +12 -0
- package/dist/adapters/jira-cloud/jira-write.adapter.js +17 -0
- package/dist/adapters/jira-cloud/mapper.d.ts +34 -0
- package/dist/adapters/jira-cloud/mapper.js +143 -0
- package/dist/adapters/telemetry/console-telemetry.d.ts +13 -0
- package/dist/adapters/telemetry/console-telemetry.js +31 -0
- package/dist/application/get-full-issue-context.d.ts +18 -0
- package/dist/application/get-full-issue-context.js +120 -0
- package/dist/application/get-issue-context.d.ts +19 -0
- package/dist/application/get-issue-context.js +79 -0
- package/dist/application/search-issues.d.ts +22 -0
- package/dist/application/search-issues.js +93 -0
- package/dist/bootstrap/boot-health-gate.d.ts +25 -0
- package/dist/bootstrap/boot-health-gate.js +137 -0
- package/dist/bootstrap/bootstrap-orchestrator.d.ts +15 -0
- package/dist/bootstrap/bootstrap-orchestrator.js +14 -0
- package/dist/bootstrap/host-mcp.d.ts +54 -0
- package/dist/bootstrap/host-mcp.js +98 -0
- package/dist/bootstrap/jira-projects.d.ts +16 -0
- package/dist/bootstrap/jira-projects.js +24 -0
- package/dist/bootstrap/mcp-config-merger.d.ts +58 -0
- package/dist/bootstrap/mcp-config-merger.js +119 -0
- package/dist/bootstrap/migration-target.d.ts +60 -0
- package/dist/bootstrap/migration-target.js +80 -0
- package/dist/bootstrap/project-bindings.d.ts +48 -0
- package/dist/bootstrap/project-bindings.js +112 -0
- package/dist/bootstrap/project-config-bootstrapper.d.ts +37 -0
- package/dist/bootstrap/project-config-bootstrapper.js +69 -0
- package/dist/bootstrap/project-config-resolver.d.ts +53 -0
- package/dist/bootstrap/project-config-resolver.js +59 -0
- package/dist/bootstrap/project-root-resolver.d.ts +22 -0
- package/dist/bootstrap/project-root-resolver.js +43 -0
- package/dist/bootstrap/setup-apply.d.ts +31 -0
- package/dist/bootstrap/setup-apply.js +61 -0
- package/dist/bootstrap/setup-plan.d.ts +102 -0
- package/dist/bootstrap/setup-plan.js +224 -0
- package/dist/bootstrap/setup-state.d.ts +73 -0
- package/dist/bootstrap/setup-state.js +89 -0
- package/dist/bootstrap/workspace-identity.d.ts +52 -0
- package/dist/bootstrap/workspace-identity.js +114 -0
- package/dist/cli/agent-api.d.ts +63 -0
- package/dist/cli/agent-api.js +149 -0
- package/dist/cli/auth.d.ts +39 -0
- package/dist/cli/auth.js +201 -0
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.js +31 -0
- package/dist/cli/runtime.d.ts +16 -0
- package/dist/cli/runtime.js +97 -0
- package/dist/cli/serve.d.ts +7 -0
- package/dist/cli/serve.js +23 -0
- package/dist/cli/setup-wizard.d.ts +37 -0
- package/dist/cli/setup-wizard.js +338 -0
- package/dist/cli/setup.d.ts +26 -0
- package/dist/cli/setup.js +214 -0
- package/dist/cli/ui.d.ts +118 -0
- package/dist/cli/ui.js +338 -0
- package/dist/cli-entry.d.ts +7 -0
- package/dist/cli-entry.js +124 -0
- package/dist/config/load-config.d.ts +18 -0
- package/dist/config/load-config.js +48 -0
- package/dist/config/schema.d.ts +36 -0
- package/dist/config/schema.js +74 -0
- package/dist/deps.d.ts +50 -0
- package/dist/deps.js +39 -0
- package/dist/domain/completeness.d.ts +63 -0
- package/dist/domain/completeness.js +20 -0
- package/dist/domain/context.d.ts +38 -0
- package/dist/domain/context.js +1 -0
- package/dist/domain/errors.d.ts +27 -0
- package/dist/domain/errors.js +49 -0
- package/dist/domain/issue.d.ts +35 -0
- package/dist/domain/issue.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -0
- package/dist/mcp/create-server.d.ts +9 -0
- package/dist/mcp/create-server.js +32 -0
- package/dist/mcp/tool-result.d.ts +10 -0
- package/dist/mcp/tool-result.js +33 -0
- package/dist/mcp/tools/jira-context.tool.d.ts +3 -0
- package/dist/mcp/tools/jira-context.tool.js +25 -0
- package/dist/mcp/tools/jira-full.tool.d.ts +3 -0
- package/dist/mcp/tools/jira-full.tool.js +27 -0
- package/dist/mcp/tools/jira-search.tool.d.ts +3 -0
- package/dist/mcp/tools/jira-search.tool.js +31 -0
- package/dist/policy/completeness-policy.d.ts +19 -0
- package/dist/policy/completeness-policy.js +34 -0
- package/dist/policy/consistency-policy.d.ts +13 -0
- package/dist/policy/consistency-policy.js +6 -0
- package/dist/policy/field-policy.d.ts +11 -0
- package/dist/policy/field-policy.js +26 -0
- package/dist/policy/output-budget-policy.d.ts +23 -0
- package/dist/policy/output-budget-policy.js +96 -0
- package/dist/policy/pagination-policy.d.ts +15 -0
- package/dist/policy/pagination-policy.js +13 -0
- package/dist/ports/cache.port.d.ts +5 -0
- package/dist/ports/cache.port.js +1 -0
- package/dist/ports/credentials.port.d.ts +27 -0
- package/dist/ports/credentials.port.js +1 -0
- package/dist/ports/jira-read.port.d.ts +73 -0
- package/dist/ports/jira-read.port.js +1 -0
- package/dist/ports/jira-write.port.d.ts +12 -0
- package/dist/ports/jira-write.port.js +1 -0
- package/dist/ports/telemetry.port.d.ts +13 -0
- package/dist/ports/telemetry.port.js +1 -0
- package/package.json +69 -0
package/dist/cli/ui.d.ts
ADDED
|
@@ -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
|
+
}
|