@pragma-sh/junie-plugin 1.0.3 → 1.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/.junie-plugin/plugin.json +1 -1
- package/dist/pragma-plugin.mjs +343 -46
- package/package.json +1 -1
- package/src/launch-options.test.ts +29 -0
- package/src/pragma-plugin.ts +33 -12
package/dist/pragma-plugin.mjs
CHANGED
|
@@ -1,15 +1,296 @@
|
|
|
1
|
-
// ../plugin/dist/shared/chunk-
|
|
2
|
-
var PLUGIN_API_VERSION = "1.
|
|
1
|
+
// ../plugin/dist/shared/chunk-b8hkp0vs.js
|
|
2
|
+
var PLUGIN_API_VERSION = "1.2.0";
|
|
3
3
|
|
|
4
|
-
// ../plugin/dist/shared/chunk-
|
|
4
|
+
// ../plugin/dist/shared/chunk-whqa76q3.js
|
|
5
5
|
function defineAgent(input) {
|
|
6
6
|
return input;
|
|
7
7
|
}
|
|
8
|
+
function dedupeSlashCommands(commands) {
|
|
9
|
+
const seen = new Set;
|
|
10
|
+
return commands.filter((command) => {
|
|
11
|
+
if (!command.name || seen.has(command.name))
|
|
12
|
+
return false;
|
|
13
|
+
seen.add(command.name);
|
|
14
|
+
return true;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
var DEFAULT_TIMEOUT_SECONDS = 30;
|
|
18
|
+
var ACP_COMMANDS_TTL_MS = 5 * 60000;
|
|
19
|
+
var MAX_CONCURRENT_PROBES = 2;
|
|
20
|
+
var SETTLE_SECONDS = "1.5";
|
|
21
|
+
var UPDATE_MARKER = "available_commands_update";
|
|
22
|
+
var STATE_KEY = Symbol.for("pragma.acpDiscovery");
|
|
23
|
+
var state = globalThis[STATE_KEY] ??= { cache: new Map, inflight: new Map, running: 0, waiting: [] };
|
|
24
|
+
var { cache, inflight } = state;
|
|
25
|
+
async function discoverAcpSlashCommands(ctx, source) {
|
|
26
|
+
const cwd = ctx.project?.path ?? "/tmp";
|
|
27
|
+
const key = `${cwd}\x00${source.command.join("\x00")}`;
|
|
28
|
+
const cached = cache.get(key);
|
|
29
|
+
if (cached && Date.now() - cached.at < ACP_COMMANDS_TTL_MS)
|
|
30
|
+
return cached.commands;
|
|
31
|
+
const probe = inflight.get(key) ?? startProbe(ctx, source, cwd, key);
|
|
32
|
+
return cached ? cached.commands : probe;
|
|
33
|
+
}
|
|
34
|
+
function startProbe(ctx, source, cwd, key) {
|
|
35
|
+
const probe = withProbeSlot(async () => {
|
|
36
|
+
const [result] = await ctx.sdk.exec.run({ cwd, commands: [acpCommandsScript(source)] });
|
|
37
|
+
return result?.status === 0 ? parseAcpCommands(result.stdout) : [];
|
|
38
|
+
}).catch(() => []).then((commands) => {
|
|
39
|
+
if (commands.length > 0)
|
|
40
|
+
cache.set(key, { commands, at: Date.now() });
|
|
41
|
+
return commands;
|
|
42
|
+
}).finally(() => inflight.delete(key));
|
|
43
|
+
inflight.set(key, probe);
|
|
44
|
+
return probe;
|
|
45
|
+
}
|
|
46
|
+
async function withProbeSlot(work) {
|
|
47
|
+
if (state.running >= MAX_CONCURRENT_PROBES) {
|
|
48
|
+
await new Promise((resolve) => state.waiting.push(resolve));
|
|
49
|
+
}
|
|
50
|
+
state.running += 1;
|
|
51
|
+
try {
|
|
52
|
+
return await work();
|
|
53
|
+
} finally {
|
|
54
|
+
state.running -= 1;
|
|
55
|
+
state.waiting.shift()?.();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function acpCommandsScript(source) {
|
|
59
|
+
const ticks = Math.round((source.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 10);
|
|
60
|
+
const [binary] = source.command;
|
|
61
|
+
const initialize = JSON.stringify({
|
|
62
|
+
jsonrpc: "2.0",
|
|
63
|
+
id: 1,
|
|
64
|
+
method: "initialize",
|
|
65
|
+
params: { protocolVersion: 1, clientCapabilities: {} }
|
|
66
|
+
});
|
|
67
|
+
return [
|
|
68
|
+
`command -v ${shellQuote(binary ?? "")} >/dev/null 2>&1 || exit 0`,
|
|
69
|
+
"work=$(mktemp -d) || exit 0",
|
|
70
|
+
`trap 'rm -rf "$work"' 0`,
|
|
71
|
+
'mkfifo "$work/in" || exit 0',
|
|
72
|
+
': >"$work/out"',
|
|
73
|
+
"{",
|
|
74
|
+
` printf '%s\\n' ${shellQuote(initialize)}`,
|
|
75
|
+
" n=0",
|
|
76
|
+
` while ! grep -q '"id":1' "$work/out" && [ ! -e "$work/done" ] && [ "$n" -lt ${ticks} ]; do sleep 0.1; n=$((n + 1)); done`,
|
|
77
|
+
` printf '{"jsonrpc":"2.0","id":2,"method":"session/new","params":{"cwd":"%s","mcpServers":[]}}\\n' "$PWD"`,
|
|
78
|
+
` while [ ! -e "$work/done" ] && [ "$n" -lt ${ticks * 2} ]; do sleep 0.1; n=$((n + 1)); done`,
|
|
79
|
+
'} >"$work/in" &',
|
|
80
|
+
"writer=$!",
|
|
81
|
+
`${source.command.map(shellArg).join(" ")} <"$work/in" >"$work/out" 2>/dev/null &`,
|
|
82
|
+
"agent=$!",
|
|
83
|
+
"n=0",
|
|
84
|
+
`while [ "$n" -lt ${ticks} ]; do`,
|
|
85
|
+
` if grep -q ${UPDATE_MARKER} "$work/out"; then sleep ${SETTLE_SECONDS}; break; fi`,
|
|
86
|
+
" sleep 0.1; n=$((n + 1))",
|
|
87
|
+
"done",
|
|
88
|
+
': >"$work/done"',
|
|
89
|
+
'kill "$agent" 2>/dev/null',
|
|
90
|
+
'wait "$writer" 2>/dev/null',
|
|
91
|
+
`grep ${UPDATE_MARKER} "$work/out" | tail -n 1`,
|
|
92
|
+
"exit 0"
|
|
93
|
+
].join(`
|
|
94
|
+
`);
|
|
95
|
+
}
|
|
96
|
+
function parseAcpCommands(stdout) {
|
|
97
|
+
let commands = [];
|
|
98
|
+
for (const line of stdout.split(`
|
|
99
|
+
`)) {
|
|
100
|
+
if (!line.includes(UPDATE_MARKER))
|
|
101
|
+
continue;
|
|
102
|
+
const parsed = updateCommands(line);
|
|
103
|
+
if (parsed)
|
|
104
|
+
commands = parsed;
|
|
105
|
+
}
|
|
106
|
+
return commands;
|
|
107
|
+
}
|
|
108
|
+
function updateCommands(line) {
|
|
109
|
+
let message;
|
|
110
|
+
try {
|
|
111
|
+
message = JSON.parse(line);
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const update = record(record(record(message)?.params)?.update);
|
|
116
|
+
if (update?.sessionUpdate !== UPDATE_MARKER || !Array.isArray(update.availableCommands)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return update.availableCommands.flatMap((entry) => {
|
|
120
|
+
const command = acpCommand(entry);
|
|
121
|
+
return command ? [command] : [];
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function acpCommand(entry) {
|
|
125
|
+
const command = record(entry);
|
|
126
|
+
const name = text(command?.name).replace(/^\//, "");
|
|
127
|
+
if (!name)
|
|
128
|
+
return null;
|
|
129
|
+
const description = text(command?.description);
|
|
130
|
+
const argumentHint = text(record(command?.input)?.hint);
|
|
131
|
+
return {
|
|
132
|
+
name,
|
|
133
|
+
...description ? { description } : {},
|
|
134
|
+
...argumentHint ? { argumentHint } : {}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function text(value) {
|
|
138
|
+
return typeof value === "string" ? value : "";
|
|
139
|
+
}
|
|
140
|
+
function record(value) {
|
|
141
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
142
|
+
}
|
|
143
|
+
function shellArg(value) {
|
|
144
|
+
return value.split("{tmp}").map((part) => part ? shellQuote(part) : "").join('"$work/tmp"');
|
|
145
|
+
}
|
|
146
|
+
function shellQuote(value) {
|
|
147
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
148
|
+
}
|
|
149
|
+
var RECORD_SEPARATOR = "\x1E";
|
|
150
|
+
var HEAD_LINES = 40;
|
|
151
|
+
var MAX_DEPTH = 8;
|
|
152
|
+
async function discoverMarkdownEntries(ctx, sources) {
|
|
153
|
+
if (sources.length === 0)
|
|
154
|
+
return [];
|
|
155
|
+
const [result] = await ctx.sdk.exec.run({
|
|
156
|
+
cwd: ctx.project?.path ?? "/tmp",
|
|
157
|
+
commands: [markdownScanScript(sources)]
|
|
158
|
+
});
|
|
159
|
+
if (!result || result.status !== 0)
|
|
160
|
+
return [];
|
|
161
|
+
return parseMarkdownScan(result.stdout, sources);
|
|
162
|
+
}
|
|
163
|
+
async function discoverSlashCommands(ctx, sources) {
|
|
164
|
+
return markdownSlashCommands(await discoverMarkdownEntries(ctx, sources));
|
|
165
|
+
}
|
|
166
|
+
function markdownSlashCommands(entries) {
|
|
167
|
+
const seen = new Set;
|
|
168
|
+
const commands = [];
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
if (entry.frontmatter["user-invocable"] === "false" || seen.has(entry.name))
|
|
171
|
+
continue;
|
|
172
|
+
seen.add(entry.name);
|
|
173
|
+
const description = entry.frontmatter.description;
|
|
174
|
+
const argumentHint = entry.frontmatter["argument-hint"];
|
|
175
|
+
commands.push({
|
|
176
|
+
name: entry.name,
|
|
177
|
+
...description ? { description } : {},
|
|
178
|
+
...argumentHint ? { argumentHint } : {}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return commands;
|
|
182
|
+
}
|
|
183
|
+
function commandAndSkillDirs(roots) {
|
|
184
|
+
return roots.flatMap((root) => [
|
|
185
|
+
{ dir: `${root}/commands`, layout: "files" },
|
|
186
|
+
{ dir: `${root}/skills`, layout: "skills" }
|
|
187
|
+
]);
|
|
188
|
+
}
|
|
189
|
+
var noCommands = () => [];
|
|
190
|
+
function slashCommandProvider(builtins, sources, discovery = {}) {
|
|
191
|
+
return async (ctx) => {
|
|
192
|
+
const [acp, loaded, files] = await Promise.all([
|
|
193
|
+
discovery.acp ? discoverAcpSlashCommands(ctx, discovery.acp).catch(noCommands) : [],
|
|
194
|
+
discovery.load ? discovery.load(ctx).catch(noCommands) : [],
|
|
195
|
+
discoverSlashCommands(ctx, sources).catch(noCommands)
|
|
196
|
+
]);
|
|
197
|
+
return dedupeSlashCommands([...builtins, ...acp, ...loaded, ...files]);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function markdownScanScript(sources) {
|
|
201
|
+
const lines = [
|
|
202
|
+
"pragma_scan() {",
|
|
203
|
+
' [ -d "$2" ] || return 0',
|
|
204
|
+
' find -L "$2" -maxdepth "$3" -type f -name "$4" 2>/dev/null | sort | while IFS= read -r f; do',
|
|
205
|
+
` printf '\\036%s\\t%s\\n' "$1" "\${f#"$2"/}"; head -n ${HEAD_LINES} "$f" 2>/dev/null; printf '\\n'`,
|
|
206
|
+
" done",
|
|
207
|
+
"}"
|
|
208
|
+
];
|
|
209
|
+
sources.forEach((source, index) => {
|
|
210
|
+
const skills = source.layout === "skills";
|
|
211
|
+
const depth = skills ? 2 : source.recursive === false ? 1 : MAX_DEPTH;
|
|
212
|
+
const pattern = skills ? "SKILL.md" : `*${source.suffix ?? ".md"}`;
|
|
213
|
+
lines.push(`pragma_scan ${index} ${shellDir(source.dir)} ${depth} ${shellQuote2(pattern)}`);
|
|
214
|
+
});
|
|
215
|
+
lines.push("exit 0");
|
|
216
|
+
return lines.join(`
|
|
217
|
+
`);
|
|
218
|
+
}
|
|
219
|
+
function parseMarkdownScan(stdout, sources) {
|
|
220
|
+
const entries = [];
|
|
221
|
+
for (const record2 of stdout.split(RECORD_SEPARATOR).slice(1)) {
|
|
222
|
+
const newline = record2.indexOf(`
|
|
223
|
+
`);
|
|
224
|
+
const header = newline === -1 ? record2 : record2.slice(0, newline);
|
|
225
|
+
const [indexText, relativePath] = header.split("\t");
|
|
226
|
+
const source = sources[Number(indexText)];
|
|
227
|
+
if (!source || !relativePath)
|
|
228
|
+
continue;
|
|
229
|
+
const frontmatter = parseFrontmatter(newline === -1 ? "" : record2.slice(newline + 1));
|
|
230
|
+
const name = entryName(source, relativePath, frontmatter);
|
|
231
|
+
if (name)
|
|
232
|
+
entries.push({ name: `${source.prefix ?? ""}${name}`, frontmatter, source });
|
|
233
|
+
}
|
|
234
|
+
return entries;
|
|
235
|
+
}
|
|
236
|
+
function parseFrontmatter(content) {
|
|
237
|
+
const lines = content.split(/\r?\n/);
|
|
238
|
+
if (lines[0]?.trim() !== "---")
|
|
239
|
+
return {};
|
|
240
|
+
const result = {};
|
|
241
|
+
let blockKey = null;
|
|
242
|
+
for (const line of lines.slice(1)) {
|
|
243
|
+
if (line.trim() === "---")
|
|
244
|
+
break;
|
|
245
|
+
if (blockKey && /^\s+\S/.test(line)) {
|
|
246
|
+
const previous = result[blockKey];
|
|
247
|
+
result[blockKey] = previous ? `${previous} ${line.trim()}` : line.trim();
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
blockKey = null;
|
|
251
|
+
const match = /^([A-Za-z][\w-]*):\s*(.*)$/.exec(line);
|
|
252
|
+
if (!match)
|
|
253
|
+
continue;
|
|
254
|
+
const value = unquote(match[2].trim());
|
|
255
|
+
if (/^[|>][+-]?$/.test(value)) {
|
|
256
|
+
blockKey = match[1];
|
|
257
|
+
} else if (value) {
|
|
258
|
+
result[match[1]] = value;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
function entryName(source, relativePath, frontmatter) {
|
|
264
|
+
if (source.layout === "skills") {
|
|
265
|
+
const directory = relativePath.split("/").slice(0, -1).join("/");
|
|
266
|
+
return frontmatter.name ?? (directory || null);
|
|
267
|
+
}
|
|
268
|
+
if (source.nameFromFrontmatter && frontmatter.name)
|
|
269
|
+
return frontmatter.name;
|
|
270
|
+
const suffix = source.suffix ?? ".md";
|
|
271
|
+
if (!relativePath.endsWith(suffix))
|
|
272
|
+
return null;
|
|
273
|
+
return relativePath.slice(0, -suffix.length).split("/").join(":") || null;
|
|
274
|
+
}
|
|
275
|
+
function unquote(value) {
|
|
276
|
+
const quoted = /^(["'])(.*)\1$/.exec(value);
|
|
277
|
+
return quoted ? quoted[2] : value;
|
|
278
|
+
}
|
|
279
|
+
function shellDir(dir) {
|
|
280
|
+
if (dir === "~")
|
|
281
|
+
return '"$HOME"';
|
|
282
|
+
if (dir.startsWith("~/"))
|
|
283
|
+
return `"$HOME"/${shellQuote2(dir.slice(2))}`;
|
|
284
|
+
return shellQuote2(dir);
|
|
285
|
+
}
|
|
286
|
+
function shellQuote2(value) {
|
|
287
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
288
|
+
}
|
|
8
289
|
function definePlugin(input) {
|
|
9
290
|
return { ...input, __apiVersion: PLUGIN_API_VERSION };
|
|
10
291
|
}
|
|
11
|
-
function
|
|
12
|
-
return
|
|
292
|
+
function defineAccounts(providers) {
|
|
293
|
+
return providers;
|
|
13
294
|
}
|
|
14
295
|
|
|
15
296
|
// ../watcher-kit/src/index.ts
|
|
@@ -210,8 +491,8 @@ async function finalizeQuestionAnswers(ctx, runtime) {
|
|
|
210
491
|
if (!ctx.signal.aborted)
|
|
211
492
|
await writeKeys(ctx, runtime.questionFinalizeKeys);
|
|
212
493
|
}
|
|
213
|
-
async function handleInterjection(ctx, submitKeys, submitDelayMs, mode,
|
|
214
|
-
const input = mode === "plain" ?
|
|
494
|
+
async function handleInterjection(ctx, submitKeys, submitDelayMs, mode, text2) {
|
|
495
|
+
const input = mode === "plain" ? text2 : `${BRACKETED_PASTE_START}${text2}${BRACKETED_PASTE_END}`;
|
|
215
496
|
await writeKeys(ctx, input);
|
|
216
497
|
if (!submitKeys) {
|
|
217
498
|
return;
|
|
@@ -229,15 +510,15 @@ function rememberCommandAttention(runtime, event) {
|
|
|
229
510
|
runtime.outstandingCommandRequestId = null;
|
|
230
511
|
}
|
|
231
512
|
}
|
|
232
|
-
function rememberQuestion(
|
|
513
|
+
function rememberQuestion(cache2, event) {
|
|
233
514
|
if (event.status === "attention" && event.attentionKind === "question" && typeof event.requestId === "string" && event.requestId.length > 0) {
|
|
234
515
|
const entries = (event.questions ?? []).flatMap(questionEntry);
|
|
235
516
|
if (entries.length > 0) {
|
|
236
|
-
|
|
517
|
+
cache2.set(event.requestId, { questions: entries });
|
|
237
518
|
return;
|
|
238
519
|
}
|
|
239
520
|
const options = (event.options ?? []).map((option) => option.label).filter((option) => option.trim() !== "");
|
|
240
|
-
|
|
521
|
+
cache2.set(event.requestId, {
|
|
241
522
|
questions: [{ question: event.question ?? "", options }]
|
|
242
523
|
});
|
|
243
524
|
}
|
|
@@ -246,12 +527,12 @@ function questionEntry(value) {
|
|
|
246
527
|
if (typeof value !== "object" || value === null || !("question" in value)) {
|
|
247
528
|
return [];
|
|
248
529
|
}
|
|
249
|
-
const
|
|
250
|
-
if (typeof
|
|
530
|
+
const record2 = value;
|
|
531
|
+
if (typeof record2.question !== "string" || !record2.question.trim()) {
|
|
251
532
|
return [];
|
|
252
533
|
}
|
|
253
|
-
const options = Array.isArray(
|
|
254
|
-
return [{ question:
|
|
534
|
+
const options = Array.isArray(record2.options) ? record2.options.filter((option) => Boolean(option) && typeof option === "object").map((option) => option.label).filter((label) => typeof label === "string" && label.trim() !== "") : [];
|
|
535
|
+
return [{ question: record2.question, options }];
|
|
255
536
|
}
|
|
256
537
|
function questionAnswerKeys(input) {
|
|
257
538
|
if (input.dismissed || input.reply === null) {
|
|
@@ -340,7 +621,7 @@ function buildCommand(usage) {
|
|
|
340
621
|
`trap 'rm -rf "$work"' 0;`,
|
|
341
622
|
'mkfifo "$work/in" || exit 1;',
|
|
342
623
|
"{",
|
|
343
|
-
`printf '%s\\n' ${
|
|
624
|
+
`printf '%s\\n' ${shellQuote3(request(INITIALIZE_ID, "initialize", { protocolVersion: 1, clientCapabilities: {} }))};`,
|
|
344
625
|
`printf '%s\\n' '{"jsonrpc":"2.0","id":${SESSION_ID},"method":"session/new","params":{"cwd":"'"$PWD"'","mcpServers":[]}}';`,
|
|
345
626
|
...usage ? [
|
|
346
627
|
"n=0;",
|
|
@@ -379,14 +660,14 @@ function findResponse(stdout, id) {
|
|
|
379
660
|
} catch {
|
|
380
661
|
continue;
|
|
381
662
|
}
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
663
|
+
const record2 = asRecord(message);
|
|
664
|
+
if (record2 === null || record2.id !== id) {
|
|
384
665
|
continue;
|
|
385
666
|
}
|
|
386
|
-
if ("result" in
|
|
387
|
-
return { ok: true, result:
|
|
667
|
+
if ("result" in record2) {
|
|
668
|
+
return { ok: true, result: record2.result };
|
|
388
669
|
}
|
|
389
|
-
return { ok: false, message: asText(asRecord(
|
|
670
|
+
return { ok: false, message: asText(asRecord(record2.error)?.message) };
|
|
390
671
|
}
|
|
391
672
|
return;
|
|
392
673
|
}
|
|
@@ -404,14 +685,14 @@ function collectAgentText(stdout) {
|
|
|
404
685
|
if (update?.sessionUpdate !== "agent_message_chunk") {
|
|
405
686
|
continue;
|
|
406
687
|
}
|
|
407
|
-
const
|
|
408
|
-
if (
|
|
409
|
-
parts.push(
|
|
688
|
+
const text2 = asText(asRecord(update.content)?.text);
|
|
689
|
+
if (text2 !== null) {
|
|
690
|
+
parts.push(text2);
|
|
410
691
|
}
|
|
411
692
|
}
|
|
412
693
|
return parts.join("");
|
|
413
694
|
}
|
|
414
|
-
function
|
|
695
|
+
function shellQuote3(value) {
|
|
415
696
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
416
697
|
}
|
|
417
698
|
function asRecord(value) {
|
|
@@ -466,14 +747,14 @@ async function loadJunieUsageLimits(ctx) {
|
|
|
466
747
|
const peak = await recordPeak(ctx, peakKey(usage), usage.remaining);
|
|
467
748
|
return buildResult(usage, peak, Date.now());
|
|
468
749
|
}
|
|
469
|
-
function parseJunieUsage(
|
|
470
|
-
const licenseType = /^\s*-?\s*License:\s*(.+?)\s*$/m.exec(
|
|
471
|
-
const money = /^\s*-?\s*Balance left:\s*([^\d\s]*)\s*([\d.,]+)\s*$/m.exec(
|
|
750
|
+
function parseJunieUsage(text2) {
|
|
751
|
+
const licenseType = /^\s*-?\s*License:\s*(.+?)\s*$/m.exec(text2)?.[1] ?? null;
|
|
752
|
+
const money = /^\s*-?\s*Balance left:\s*([^\d\s]*)\s*([\d.,]+)\s*$/m.exec(text2);
|
|
472
753
|
if (money) {
|
|
473
754
|
const remaining = parseAmount(money[2]);
|
|
474
755
|
return remaining === null ? null : { licenseType, remaining, unit: money[1] || "$" };
|
|
475
756
|
}
|
|
476
|
-
const credits = /^\s*-?\s*Quota:\s*([\d.,]+)\s*(\S+)?\s*remaining\s*$/m.exec(
|
|
757
|
+
const credits = /^\s*-?\s*Quota:\s*([\d.,]+)\s*(\S+)?\s*remaining\s*$/m.exec(text2);
|
|
477
758
|
if (credits) {
|
|
478
759
|
const remaining = parseAmount(credits[1]);
|
|
479
760
|
return remaining === null ? null : { licenseType, remaining, unit: credits[2] ?? "credits" };
|
|
@@ -526,14 +807,14 @@ async function readPeaksText(ctx, cwd) {
|
|
|
526
807
|
async function writePeaksText(ctx, cwd, contents) {
|
|
527
808
|
const [result] = await ctx.sdk.exec.run({
|
|
528
809
|
cwd,
|
|
529
|
-
commands: [`mkdir -p ${PEAKS_DIR} && printf '%s\\n' ${
|
|
810
|
+
commands: [`mkdir -p ${PEAKS_DIR} && printf '%s\\n' ${shellQuote3(contents)} > ${PEAKS_FILE}`]
|
|
530
811
|
});
|
|
531
812
|
if (!result || result.status !== 0) {
|
|
532
813
|
throw new Error(result?.stderr.trim() || "failed to write Junie usage cache");
|
|
533
814
|
}
|
|
534
815
|
}
|
|
535
|
-
function parsePeaksFile(
|
|
536
|
-
const trimmed =
|
|
816
|
+
function parsePeaksFile(text2) {
|
|
817
|
+
const trimmed = text2.trim();
|
|
537
818
|
if (trimmed === "") {
|
|
538
819
|
return { version: PEAKS_VERSION, peaks: {} };
|
|
539
820
|
}
|
|
@@ -542,12 +823,12 @@ function parsePeaksFile(text) {
|
|
|
542
823
|
if (typeof parsed !== "object" || parsed === null) {
|
|
543
824
|
return { version: PEAKS_VERSION, peaks: {} };
|
|
544
825
|
}
|
|
545
|
-
const
|
|
546
|
-
if (
|
|
826
|
+
const record2 = parsed;
|
|
827
|
+
if (record2.version !== PEAKS_VERSION || typeof record2.peaks !== "object" || record2.peaks === null) {
|
|
547
828
|
return { version: PEAKS_VERSION, peaks: {} };
|
|
548
829
|
}
|
|
549
830
|
const peaks = {};
|
|
550
|
-
for (const [key, value] of Object.entries(
|
|
831
|
+
for (const [key, value] of Object.entries(record2.peaks)) {
|
|
551
832
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
552
833
|
peaks[key] = value;
|
|
553
834
|
}
|
|
@@ -579,20 +860,28 @@ var baseWatcher = createTuiWatcher({
|
|
|
579
860
|
questionSelectMode: "arrow-space",
|
|
580
861
|
questionFinalizeKeys: "\r"
|
|
581
862
|
});
|
|
863
|
+
var BUILTIN_SLASH_COMMANDS = [
|
|
864
|
+
{ name: "review", description: "Local code review of your changes" },
|
|
865
|
+
{ name: "plan", description: "Plan mode", argumentHint: "[prompt]" },
|
|
866
|
+
{ name: "usage", description: "Show the session cost breakdown" }
|
|
867
|
+
];
|
|
868
|
+
var SLASH_COMMAND_SOURCES = commandAndSkillDirs([".junie", "~/.junie"]);
|
|
582
869
|
var junieAgentPlugin = definePlugin({
|
|
583
870
|
name: "Junie",
|
|
584
871
|
description: "Launch the JetBrains Junie CLI from Pragma.",
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
872
|
+
accounts: defineAccounts([
|
|
873
|
+
{
|
|
874
|
+
provider: "jetbrains",
|
|
875
|
+
agent: "junie",
|
|
589
876
|
dashboardUrl: "https://junie.jetbrains.com/cli",
|
|
590
877
|
iconPath: "assets/junie.svg",
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
878
|
+
usageLimits: {
|
|
879
|
+
primaryLimitId: PRIMARY_LIMIT_ID,
|
|
880
|
+
refreshIntervalMs: USAGE_REFRESH_INTERVAL_MS,
|
|
881
|
+
load: loadJunieUsageLimits
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
]),
|
|
596
885
|
watchers: [
|
|
597
886
|
{
|
|
598
887
|
agent: "junie",
|
|
@@ -626,13 +915,21 @@ var junieAgentPlugin = definePlugin({
|
|
|
626
915
|
models: loadJunieModels,
|
|
627
916
|
permissionModes: [
|
|
628
917
|
{ id: "default", name: "Ask for approval" },
|
|
918
|
+
{ id: "plan", name: "Plan mode" },
|
|
629
919
|
{ id: "brave", name: "Brave mode (no approvals)" }
|
|
630
920
|
],
|
|
921
|
+
slashCommands: slashCommandProvider(BUILTIN_SLASH_COMMANDS, SLASH_COMMAND_SOURCES, {
|
|
922
|
+
acp: { command: ["junie", "--acp=true", "--skip-update-check", "--cache-dir={tmp}"] }
|
|
923
|
+
}),
|
|
631
924
|
excludeFeatures: ["subagents"],
|
|
632
925
|
args: {
|
|
633
926
|
model: (modelId) => ["--model", modelId],
|
|
634
927
|
reasoning: (reasoningId) => ["--effort", reasoningId],
|
|
635
|
-
permissionMode: (permissionModeId) =>
|
|
928
|
+
permissionMode: (permissionModeId) => {
|
|
929
|
+
if (permissionModeId === "brave")
|
|
930
|
+
return ["--brave"];
|
|
931
|
+
return permissionModeId === "plan" ? ["--plan"] : [];
|
|
932
|
+
}
|
|
636
933
|
}
|
|
637
934
|
})
|
|
638
935
|
]
|
|
@@ -683,8 +980,8 @@ function records(value) {
|
|
|
683
980
|
return [];
|
|
684
981
|
}
|
|
685
982
|
return value.flatMap((entry) => {
|
|
686
|
-
const
|
|
687
|
-
return
|
|
983
|
+
const record2 = asRecord(entry);
|
|
984
|
+
return record2 === null ? [] : [record2];
|
|
688
985
|
});
|
|
689
986
|
}
|
|
690
987
|
function titleCase(value) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
agentLaunchArgs,
|
|
5
|
+
resolveAgentOptions,
|
|
6
|
+
type PluginContext,
|
|
7
|
+
} from "@pragma-sh/plugin/catalog";
|
|
8
|
+
|
|
9
|
+
import { junieAgentPlugin } from "./pragma-plugin";
|
|
10
|
+
|
|
11
|
+
/** A context whose host shell finds no command or agent files. */
|
|
12
|
+
const emptyHost = {
|
|
13
|
+
project: { id: "p", name: "p", path: "/repo" },
|
|
14
|
+
sdk: { exec: { run: async () => [{ status: 0, stdout: "", stderr: "" }] } },
|
|
15
|
+
} as unknown as PluginContext;
|
|
16
|
+
|
|
17
|
+
const agent = junieAgentPlugin.agents![0]!;
|
|
18
|
+
|
|
19
|
+
describe("Junie launch options", () => {
|
|
20
|
+
it("exposes built-in slash commands", async () => {
|
|
21
|
+
const { slashCommands } = await resolveAgentOptions(agent, emptyHost);
|
|
22
|
+
expect(slashCommands.map((command) => command.name)).toEqual(["review", "plan", "usage"]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("maps plan and brave permission modes to launch flags", () => {
|
|
26
|
+
expect(agentLaunchArgs(agent, { permissionModeId: "plan" })).toEqual(["--plan"]);
|
|
27
|
+
expect(agentLaunchArgs(agent, { permissionModeId: "brave" })).toEqual(["--brave"]);
|
|
28
|
+
});
|
|
29
|
+
});
|
package/src/pragma-plugin.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
defineAccounts,
|
|
2
3
|
defineAgent,
|
|
3
4
|
definePlugin,
|
|
4
|
-
|
|
5
|
+
commandAndSkillDirs,
|
|
6
|
+
slashCommandProvider,
|
|
5
7
|
type AgentModelEntry,
|
|
6
8
|
type PluginContext,
|
|
7
9
|
type PluginDefinition,
|
|
@@ -34,6 +36,15 @@ const baseWatcher = createTuiWatcher({
|
|
|
34
36
|
questionFinalizeKeys: "\r",
|
|
35
37
|
});
|
|
36
38
|
|
|
39
|
+
/** Junie built-ins worth starting a session with; custom commands are discovered. */
|
|
40
|
+
const BUILTIN_SLASH_COMMANDS = [
|
|
41
|
+
{ name: "review", description: "Local code review of your changes" },
|
|
42
|
+
{ name: "plan", description: "Plan mode", argumentHint: "[prompt]" },
|
|
43
|
+
{ name: "usage", description: "Show the session cost breakdown" },
|
|
44
|
+
];
|
|
45
|
+
/** Fallback when the ACP list is unavailable. */
|
|
46
|
+
const SLASH_COMMAND_SOURCES = commandAndSkillDirs([".junie", "~/.junie"]);
|
|
47
|
+
|
|
37
48
|
/**
|
|
38
49
|
* Pragma plugin for the JetBrains Junie CLI, bundled to `dist/pragma-plugin.mjs`.
|
|
39
50
|
*
|
|
@@ -47,17 +58,21 @@ const baseWatcher = createTuiWatcher({
|
|
|
47
58
|
export const junieAgentPlugin: PluginDefinition = definePlugin({
|
|
48
59
|
name: "Junie",
|
|
49
60
|
description: "Launch the JetBrains Junie CLI from Pragma.",
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
accounts: defineAccounts([
|
|
62
|
+
{
|
|
63
|
+
provider: "jetbrains",
|
|
64
|
+
agent: "junie",
|
|
54
65
|
dashboardUrl: "https://junie.jetbrains.com/cli",
|
|
55
66
|
iconPath: "assets/junie.svg",
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
// Junie signs in from its own welcome screen and has no config-dir
|
|
68
|
+
// override, so it has exactly one login: the one the harness owns.
|
|
69
|
+
usageLimits: {
|
|
70
|
+
primaryLimitId: PRIMARY_LIMIT_ID,
|
|
71
|
+
refreshIntervalMs: USAGE_REFRESH_INTERVAL_MS,
|
|
72
|
+
load: loadJunieUsageLimits,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
]),
|
|
61
76
|
watchers: [
|
|
62
77
|
{
|
|
63
78
|
agent: "junie",
|
|
@@ -98,8 +113,12 @@ export const junieAgentPlugin: PluginDefinition = definePlugin({
|
|
|
98
113
|
// off keeps whatever the user configured, which defaults to `auto`.
|
|
99
114
|
permissionModes: [
|
|
100
115
|
{ id: "default", name: "Ask for approval" },
|
|
116
|
+
{ id: "plan", name: "Plan mode" },
|
|
101
117
|
{ id: "brave", name: "Brave mode (no approvals)" },
|
|
102
118
|
],
|
|
119
|
+
slashCommands: slashCommandProvider(BUILTIN_SLASH_COMMANDS, SLASH_COMMAND_SOURCES, {
|
|
120
|
+
acp: { command: ["junie", "--acp=true", "--skip-update-check", "--cache-dir={tmp}"] },
|
|
121
|
+
}),
|
|
103
122
|
// `subagents`: Junie runs subagents inside the same process and fires no
|
|
104
123
|
// per-agent hook (`PreToolUse` carries no agent or session id), so their
|
|
105
124
|
// start and finish are not observable from a hook bridge.
|
|
@@ -107,8 +126,10 @@ export const junieAgentPlugin: PluginDefinition = definePlugin({
|
|
|
107
126
|
args: {
|
|
108
127
|
model: (modelId: string) => ["--model", modelId],
|
|
109
128
|
reasoning: (reasoningId: string) => ["--effort", reasoningId],
|
|
110
|
-
permissionMode: (permissionModeId: string) =>
|
|
111
|
-
permissionModeId === "brave"
|
|
129
|
+
permissionMode: (permissionModeId: string) => {
|
|
130
|
+
if (permissionModeId === "brave") return ["--brave"];
|
|
131
|
+
return permissionModeId === "plan" ? ["--plan"] : [];
|
|
132
|
+
},
|
|
112
133
|
},
|
|
113
134
|
}),
|
|
114
135
|
],
|