@stubbedev/atlassian-mcp 0.4.5 → 0.5.1
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/README.md +96 -42
- package/bin/cli.mjs +59 -0
- package/package.json +17 -30
- package/scripts/download.mjs +86 -0
- package/scripts/postinstall.mjs +15 -0
- package/dist/attachment.js +0 -350
- package/dist/bitbucket.js +0 -1340
- package/dist/config.js +0 -62
- package/dist/context.js +0 -162
- package/dist/git.js +0 -227
- package/dist/index.js +0 -1055
- package/dist/jira.js +0 -979
- package/dist/video.js +0 -211
package/dist/video.js
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { writeFile, readdir, readFile, mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { createHash } from 'node:crypto';
|
|
6
|
-
// ffmpeg-static / ffprobe-static ship no types; declare just enough to import.
|
|
7
|
-
// @ts-ignore - no @types
|
|
8
|
-
import ffmpegPathImport from 'ffmpeg-static';
|
|
9
|
-
// @ts-ignore - no @types
|
|
10
|
-
import ffprobeStaticImport from 'ffprobe-static';
|
|
11
|
-
const FFMPEG = process.env.ATLASSIAN_MCP_FFMPEG_PATH ||
|
|
12
|
-
ffmpegPathImport ||
|
|
13
|
-
null;
|
|
14
|
-
const FFPROBE = process.env.ATLASSIAN_MCP_FFPROBE_PATH ||
|
|
15
|
-
ffprobeStaticImport?.path ||
|
|
16
|
-
null;
|
|
17
|
-
export const DEFAULT_VIDEO_FRAMES = 6;
|
|
18
|
-
export const DEFAULT_VIDEO_MAX_DIMENSION = 768;
|
|
19
|
-
export const DEFAULT_VIDEO_QUALITY = 65;
|
|
20
|
-
export const MAX_VIDEO_SOURCE_BYTES = 250 * 1024 * 1024;
|
|
21
|
-
export const VIDEO_FRAMES_MIN = 1;
|
|
22
|
-
export const VIDEO_FRAMES_MAX = 60;
|
|
23
|
-
export const DEFAULT_SCENE_THRESHOLD = 0.3;
|
|
24
|
-
function run(cmd, args) {
|
|
25
|
-
return new Promise((resolve, reject) => {
|
|
26
|
-
const proc = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
27
|
-
const stdoutChunks = [];
|
|
28
|
-
let stderr = '';
|
|
29
|
-
proc.stdout.on('data', (c) => stdoutChunks.push(c));
|
|
30
|
-
proc.stderr.on('data', (c) => (stderr += c.toString()));
|
|
31
|
-
proc.on('error', reject);
|
|
32
|
-
proc.on('close', (code) => resolve({ stdout: Buffer.concat(stdoutChunks), stderr, code: code ?? 0 }));
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
async function probeVideo(filePath) {
|
|
36
|
-
if (!FFPROBE)
|
|
37
|
-
throw new Error('ffprobe binary unavailable. Set ATLASSIAN_MCP_FFPROBE_PATH or install ffprobe-static.');
|
|
38
|
-
const { stdout, stderr, code } = await run(FFPROBE, [
|
|
39
|
-
'-v', 'error',
|
|
40
|
-
'-select_streams', 'v:0',
|
|
41
|
-
'-show_entries', 'stream=width,height,r_frame_rate,codec_name,duration,nb_frames,nb_read_frames:format=duration',
|
|
42
|
-
'-count_frames', // populate nb_read_frames for containers without nb_frames (e.g. GIF)
|
|
43
|
-
'-of', 'json',
|
|
44
|
-
filePath,
|
|
45
|
-
]);
|
|
46
|
-
if (code !== 0)
|
|
47
|
-
throw new Error(`ffprobe failed (${code}): ${stderr.trim() || 'unknown error'}`);
|
|
48
|
-
const json = JSON.parse(stdout.toString('utf-8'));
|
|
49
|
-
const stream = json.streams?.[0];
|
|
50
|
-
if (!stream)
|
|
51
|
-
throw new Error('No video stream found.');
|
|
52
|
-
const rate = stream.r_frame_rate ?? '0/1';
|
|
53
|
-
const [num, den] = rate.split('/').map((s) => parseFloat(s));
|
|
54
|
-
const fps = Number.isFinite(num) && Number.isFinite(den) && den > 0 ? num / den : 0;
|
|
55
|
-
// duration fallback chain: format.duration → stream.duration → nb_frames/fps → 0
|
|
56
|
-
const formatDuration = parseFloat(json.format?.duration ?? '');
|
|
57
|
-
const streamDuration = parseFloat(stream.duration ?? '');
|
|
58
|
-
const frameCount = parseInt(stream.nb_frames ?? stream.nb_read_frames ?? '', 10);
|
|
59
|
-
const fromFrames = Number.isFinite(frameCount) && frameCount > 0 && fps > 0 ? frameCount / fps : 0;
|
|
60
|
-
const duration = [formatDuration, streamDuration, fromFrames].find((v) => Number.isFinite(v) && v > 0) ?? 0;
|
|
61
|
-
return {
|
|
62
|
-
duration,
|
|
63
|
-
width: stream.width ?? 0,
|
|
64
|
-
height: stream.height ?? 0,
|
|
65
|
-
fps,
|
|
66
|
-
codec: stream.codec_name ?? 'unknown',
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
// Quick content-derived cache key. Hashes head+tail+length to avoid full-buffer scan on large videos.
|
|
70
|
-
function quickHash(buf) {
|
|
71
|
-
const h = createHash('sha1');
|
|
72
|
-
const HEAD = 1024 * 1024;
|
|
73
|
-
h.update(buf.subarray(0, Math.min(buf.length, HEAD)));
|
|
74
|
-
if (buf.length > HEAD * 2)
|
|
75
|
-
h.update(buf.subarray(buf.length - HEAD));
|
|
76
|
-
h.update(String(buf.length));
|
|
77
|
-
return h.digest('hex');
|
|
78
|
-
}
|
|
79
|
-
const VIDEO_CACHE = new Map();
|
|
80
|
-
const VIDEO_CACHE_MAX = 16;
|
|
81
|
-
function cacheGet(key) {
|
|
82
|
-
const hit = VIDEO_CACHE.get(key);
|
|
83
|
-
if (!hit)
|
|
84
|
-
return undefined;
|
|
85
|
-
// refresh LRU order
|
|
86
|
-
VIDEO_CACHE.delete(key);
|
|
87
|
-
VIDEO_CACHE.set(key, hit);
|
|
88
|
-
return hit;
|
|
89
|
-
}
|
|
90
|
-
function cacheSet(key, value) {
|
|
91
|
-
if (VIDEO_CACHE.size >= VIDEO_CACHE_MAX) {
|
|
92
|
-
const oldest = VIDEO_CACHE.keys().next().value;
|
|
93
|
-
if (oldest !== undefined)
|
|
94
|
-
VIDEO_CACHE.delete(oldest);
|
|
95
|
-
}
|
|
96
|
-
VIDEO_CACHE.set(key, value);
|
|
97
|
-
}
|
|
98
|
-
// Parse pts_time from ffmpeg showinfo stderr. Returns timestamps in input order.
|
|
99
|
-
function parseShowinfoTimestamps(stderr) {
|
|
100
|
-
const out = [];
|
|
101
|
-
const re = /Parsed_showinfo[^\]]*\][^\n]*pts_time:([\d.]+)/g;
|
|
102
|
-
let m;
|
|
103
|
-
while ((m = re.exec(stderr)) !== null) {
|
|
104
|
-
const v = parseFloat(m[1]);
|
|
105
|
-
if (Number.isFinite(v))
|
|
106
|
-
out.push(v);
|
|
107
|
-
}
|
|
108
|
-
return out;
|
|
109
|
-
}
|
|
110
|
-
export async function processVideo(buffer, opts = {}) {
|
|
111
|
-
if (!FFMPEG)
|
|
112
|
-
throw new Error('ffmpeg binary unavailable. Set ATLASSIAN_MCP_FFMPEG_PATH or install ffmpeg-static.');
|
|
113
|
-
const frames = Math.max(VIDEO_FRAMES_MIN, Math.min(opts.frames ?? DEFAULT_VIDEO_FRAMES, VIDEO_FRAMES_MAX));
|
|
114
|
-
const dedup = opts.dedup ?? true;
|
|
115
|
-
const mode = opts.mode ?? 'uniform';
|
|
116
|
-
const sceneThreshold = Math.max(0.01, Math.min(opts.sceneThreshold ?? DEFAULT_SCENE_THRESHOLD, 1));
|
|
117
|
-
const cacheKey = `${quickHash(buffer)}:${frames}:${opts.start ?? 'a'}:${opts.end ?? 'z'}:${dedup}:${mode}:${sceneThreshold}`;
|
|
118
|
-
const cached = cacheGet(cacheKey);
|
|
119
|
-
if (cached)
|
|
120
|
-
return cached;
|
|
121
|
-
const dir = await mkdtemp(join(tmpdir(), 'atlmcp-video-'));
|
|
122
|
-
const input = join(dir, 'input');
|
|
123
|
-
try {
|
|
124
|
-
await writeFile(input, buffer);
|
|
125
|
-
const meta = await probeVideo(input);
|
|
126
|
-
const start = Math.max(0, opts.start ?? 0);
|
|
127
|
-
const endRaw = opts.end ?? meta.duration;
|
|
128
|
-
const end = Math.min(meta.duration > 0 ? meta.duration : endRaw, endRaw);
|
|
129
|
-
if (end <= start)
|
|
130
|
-
throw new Error(`Invalid window: start=${start}s end=${end}s (duration=${meta.duration}s).`);
|
|
131
|
-
const window = end - start;
|
|
132
|
-
const extractWithMode = async (curMode, useDedup) => {
|
|
133
|
-
const vfParts = [];
|
|
134
|
-
if (curMode === 'scenes') {
|
|
135
|
-
// Threshold (default 0.3) sets scene-change sensitivity. Output rate is non-uniform; -frames:v caps count.
|
|
136
|
-
vfParts.push(`select='gt(scene\\,${sceneThreshold.toFixed(3)})'`);
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
const fps = Math.max(frames / window, 0.001);
|
|
140
|
-
vfParts.push(`fps=${fps}`);
|
|
141
|
-
}
|
|
142
|
-
if (useDedup)
|
|
143
|
-
vfParts.push(`mpdecimate=hi=64*12:lo=64*5:frac=0.33`);
|
|
144
|
-
vfParts.push('showinfo');
|
|
145
|
-
const vf = vfParts.join(',');
|
|
146
|
-
const args = [
|
|
147
|
-
'-hide_banner', '-loglevel', 'info',
|
|
148
|
-
'-ss', start.toFixed(3),
|
|
149
|
-
'-to', end.toFixed(3),
|
|
150
|
-
'-i', input,
|
|
151
|
-
'-vf', vf,
|
|
152
|
-
'-frames:v', String(frames),
|
|
153
|
-
'-fps_mode', 'vfr',
|
|
154
|
-
'-an', '-sn',
|
|
155
|
-
'-q:v', '2',
|
|
156
|
-
join(dir, 'frame-%03d.jpg'),
|
|
157
|
-
];
|
|
158
|
-
const { stderr, code } = await run(FFMPEG, args);
|
|
159
|
-
if (code !== 0)
|
|
160
|
-
throw new Error(`ffmpeg failed (${code}): ${stderr.split('\n').filter((l) => /error/i.test(l)).slice(-3).join(' / ').trim() || 'unknown error'}`);
|
|
161
|
-
const files = (await readdir(dir)).filter((f) => f.startsWith('frame-') && f.endsWith('.jpg')).sort();
|
|
162
|
-
const ptsTimes = parseShowinfoTimestamps(stderr);
|
|
163
|
-
// showinfo emits one line per output frame; align by index.
|
|
164
|
-
const exact = ptsTimes.length === files.length;
|
|
165
|
-
const out = [];
|
|
166
|
-
const step = window / Math.max(1, files.length);
|
|
167
|
-
for (let i = 0; i < files.length; i++) {
|
|
168
|
-
const data = await readFile(join(dir, files[i]));
|
|
169
|
-
// pts_time is offset from -ss seek point; add start for wall-clock.
|
|
170
|
-
const ts = exact ? start + ptsTimes[i] : start + step * (i + 0.5);
|
|
171
|
-
out.push({ data, timestampSec: ts, approximate: !exact });
|
|
172
|
-
}
|
|
173
|
-
return { frames: out, approximate: !exact };
|
|
174
|
-
};
|
|
175
|
-
const cleanFrames = async () => {
|
|
176
|
-
for (const f of await readdir(dir)) {
|
|
177
|
-
if (f.startsWith('frame-'))
|
|
178
|
-
await rm(join(dir, f), { force: true });
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
let extracted = await extractWithMode(mode, dedup);
|
|
182
|
-
let dedupApplied = dedup;
|
|
183
|
-
let effectiveMode = mode;
|
|
184
|
-
// Scenes mode with no scene changes detected → fall back to uniform.
|
|
185
|
-
if (extracted.frames.length === 0 && mode === 'scenes') {
|
|
186
|
-
await cleanFrames();
|
|
187
|
-
effectiveMode = 'uniform';
|
|
188
|
-
extracted = await extractWithMode('uniform', dedup);
|
|
189
|
-
}
|
|
190
|
-
// Dedup killed everything → retry without dedup.
|
|
191
|
-
if (extracted.frames.length === 0 && dedup) {
|
|
192
|
-
await cleanFrames();
|
|
193
|
-
extracted = await extractWithMode(effectiveMode, false);
|
|
194
|
-
dedupApplied = false;
|
|
195
|
-
}
|
|
196
|
-
const result = {
|
|
197
|
-
meta,
|
|
198
|
-
frames: extracted.frames,
|
|
199
|
-
effectiveStart: start,
|
|
200
|
-
effectiveEnd: end,
|
|
201
|
-
dedupApplied,
|
|
202
|
-
mode: effectiveMode,
|
|
203
|
-
approximateTimestamps: extracted.approximate,
|
|
204
|
-
};
|
|
205
|
-
cacheSet(cacheKey, result);
|
|
206
|
-
return result;
|
|
207
|
-
}
|
|
208
|
-
finally {
|
|
209
|
-
await rm(dir, { recursive: true, force: true });
|
|
210
|
-
}
|
|
211
|
-
}
|