@pragma-sh/junie-plugin 1.0.2 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pragma-junie",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Reports Junie CLI agent status (running / done / attention) to Pragma automatically.",
5
5
  "author": {
6
6
  "name": "Pragma"
@@ -1,15 +1,296 @@
1
- // ../plugin/dist/shared/chunk-e222xk1w.js
2
- var PLUGIN_API_VERSION = "1.0.1";
1
+ // ../plugin/dist/shared/chunk-b8hkp0vs.js
2
+ var PLUGIN_API_VERSION = "1.2.0";
3
3
 
4
- // ../plugin/dist/shared/chunk-6qx2xj9p.js
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 defineUsageLimitProvider(input) {
12
- return input;
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, text) {
214
- const input = mode === "plain" ? text : `${BRACKETED_PASTE_START}${text}${BRACKETED_PASTE_END}`;
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(cache, event) {
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
- cache.set(event.requestId, { questions: entries });
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
- cache.set(event.requestId, {
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 record = value;
250
- if (typeof record.question !== "string" || !record.question.trim()) {
530
+ const record2 = value;
531
+ if (typeof record2.question !== "string" || !record2.question.trim()) {
251
532
  return [];
252
533
  }
253
- const options = Array.isArray(record.options) ? record.options.filter((option) => Boolean(option) && typeof option === "object").map((option) => option.label).filter((label) => typeof label === "string" && label.trim() !== "") : [];
254
- return [{ question: record.question, options }];
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' ${shellQuote(request(INITIALIZE_ID, "initialize", { protocolVersion: 1, clientCapabilities: {} }))};`,
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 record = asRecord(message);
383
- if (record === null || record.id !== id) {
663
+ const record2 = asRecord(message);
664
+ if (record2 === null || record2.id !== id) {
384
665
  continue;
385
666
  }
386
- if ("result" in record) {
387
- return { ok: true, result: record.result };
667
+ if ("result" in record2) {
668
+ return { ok: true, result: record2.result };
388
669
  }
389
- return { ok: false, message: asText(asRecord(record.error)?.message) };
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 text = asText(asRecord(update.content)?.text);
408
- if (text !== null) {
409
- parts.push(text);
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 shellQuote(value) {
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(text) {
470
- const licenseType = /^\s*-?\s*License:\s*(.+?)\s*$/m.exec(text)?.[1] ?? null;
471
- const money = /^\s*-?\s*Balance left:\s*([^\d\s]*)\s*([\d.,]+)\s*$/m.exec(text);
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(text);
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' ${shellQuote(contents)} > ${PEAKS_FILE}`]
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(text) {
536
- const trimmed = text.trim();
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 record = parsed;
546
- if (record.version !== PEAKS_VERSION || typeof record.peaks !== "object" || record.peaks === null) {
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(record.peaks)) {
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
- usageLimits: [
586
- defineUsageLimitProvider({
587
- id: "junie",
588
- title: "Junie",
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
- primaryLimitId: PRIMARY_LIMIT_ID,
592
- refreshIntervalMs: USAGE_REFRESH_INTERVAL_MS,
593
- load: loadJunieUsageLimits
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) => permissionModeId === "brave" ? ["--brave"] : []
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 record = asRecord(entry);
687
- return record === null ? [] : [record];
983
+ const record2 = asRecord(entry);
984
+ return record2 === null ? [] : [record2];
688
985
  });
689
986
  }
690
987
  function titleCase(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pragma-sh/junie-plugin",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "JetBrains Junie CLI plugin that reports agent status to Pragma via native hooks.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -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
+ });
@@ -1,7 +1,9 @@
1
1
  import {
2
+ defineAccounts,
2
3
  defineAgent,
3
4
  definePlugin,
4
- defineUsageLimitProvider,
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
- usageLimits: [
51
- defineUsageLimitProvider({
52
- id: "junie",
53
- title: "Junie",
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
- primaryLimitId: PRIMARY_LIMIT_ID,
57
- refreshIntervalMs: USAGE_REFRESH_INTERVAL_MS,
58
- load: loadJunieUsageLimits,
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" ? ["--brave"] : [],
129
+ permissionMode: (permissionModeId: string) => {
130
+ if (permissionModeId === "brave") return ["--brave"];
131
+ return permissionModeId === "plan" ? ["--plan"] : [];
132
+ },
112
133
  },
113
134
  }),
114
135
  ],