@pragma-sh/opencode-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.
package/dist/index.mjs CHANGED
@@ -1,17 +1,4 @@
1
1
  // ../sdk/dist/index.js
2
- var PRAGMA_ENV_KEYS = {
3
- gatewayUrl: "PRAGMA_GATEWAY_URL",
4
- gatewayToken: "PRAGMA_GATEWAY_TOKEN",
5
- tabId: "PRAGMA_TAB_ID",
6
- worktreeId: "PRAGMA_WORKTREE_ID"
7
- };
8
- function readEnv(name, env) {
9
- const globalProcess = globalThis.process;
10
- return env?.[name] ?? globalProcess?.env?.[name];
11
- }
12
- function hasPragmaEnvironment(env) {
13
- return Boolean(readEnv(PRAGMA_ENV_KEYS.gatewayUrl, env) && readEnv(PRAGMA_ENV_KEYS.gatewayToken, env) && readEnv(PRAGMA_ENV_KEYS.tabId, env) && readEnv(PRAGMA_ENV_KEYS.worktreeId, env));
14
- }
15
2
  var routes = {
16
3
  health: "/v1/health",
17
4
  version: "/v1/version",
@@ -39,6 +26,19 @@ var routes = {
39
26
  pushTest: "/v1/push/test",
40
27
  pushPresence: "/v1/push/presence"
41
28
  };
29
+ var PRAGMA_ENV_KEYS = {
30
+ gatewayUrl: "PRAGMA_GATEWAY_URL",
31
+ gatewayToken: "PRAGMA_GATEWAY_TOKEN",
32
+ tabId: "PRAGMA_TAB_ID",
33
+ worktreeId: "PRAGMA_WORKTREE_ID"
34
+ };
35
+ function readEnv(name, env) {
36
+ const globalProcess = globalThis.process;
37
+ return env?.[name] ?? globalProcess?.env?.[name];
38
+ }
39
+ function hasPragmaEnvironment(env) {
40
+ return Boolean(readEnv(PRAGMA_ENV_KEYS.gatewayUrl, env) && readEnv(PRAGMA_ENV_KEYS.gatewayToken, env) && readEnv(PRAGMA_ENV_KEYS.tabId, env) && readEnv(PRAGMA_ENV_KEYS.worktreeId, env));
41
+ }
42
42
 
43
43
  class PragmaGatewayError extends Error {
44
44
  code;
@@ -267,6 +267,18 @@ class AgentsClient {
267
267
  signal: options.signal
268
268
  });
269
269
  }
270
+ async launchOptions(agentId, options = {}) {
271
+ const catalog = await this.catalog(options);
272
+ const agent = catalog.agents.find((candidate) => candidate.id === agentId);
273
+ if (!agent) {
274
+ throw new Error(`agent not found in catalog: ${agentId}`);
275
+ }
276
+ return {
277
+ slashCommands: agent.slashCommands ?? [],
278
+ modes: agent.modes ?? [],
279
+ permissionModes: agent.permissionModes ?? []
280
+ };
281
+ }
270
282
  launch(payload, options = {}) {
271
283
  return this.transport.request(routes.control("agentSessionLaunch"), {
272
284
  method: "POST",
@@ -506,6 +518,7 @@ function createPragmaOpencodeHooks(reporter) {
506
518
  const messageText = new Map;
507
519
  const childSessions = new Map;
508
520
  const activeChildSessions = new Set;
521
+ const activeShellCalls = new Set;
509
522
  let pendingLegacyQuestion = null;
510
523
  let activeCanonicalQuestionId = null;
511
524
  const EVENT_HANDLERS = {
@@ -513,6 +526,8 @@ function createPragmaOpencodeHooks(reporter) {
513
526
  "session.idle": applySessionIdleEvent,
514
527
  "session.error": applySessionErrorEvent,
515
528
  "session.deleted": applySessionDeletedEvent,
529
+ "session.next.shell.started": applyShellStartedEvent,
530
+ "session.next.shell.ended": applyShellEndedEvent,
516
531
  "server.instance.disposed": () => "clear",
517
532
  "permission.replied": applyPermissionRepliedEvent,
518
533
  "question.replied": applyQuestionRepliedEvent,
@@ -703,6 +718,7 @@ function createPragmaOpencodeHooks(reporter) {
703
718
  attention = false;
704
719
  childSessions.clear();
705
720
  activeChildSessions.clear();
721
+ activeShellCalls.clear();
706
722
  if (lastReported === "cleared") {
707
723
  return;
708
724
  }
@@ -713,7 +729,7 @@ function createPragmaOpencodeHooks(reporter) {
713
729
  if (attention) {
714
730
  return `attention:${attentionKind}`;
715
731
  }
716
- if (busy) {
732
+ if (busy || activeShellCalls.size > 0) {
717
733
  return "started";
718
734
  }
719
735
  return "stopped";
@@ -761,15 +777,38 @@ function createPragmaOpencodeHooks(reporter) {
761
777
  }
762
778
  busy = false;
763
779
  attention = false;
780
+ removeShellCalls(sessionIdFromEvent(event));
764
781
  removeChildren(sessionIdFromEvent(event));
765
782
  return "sync";
766
783
  }
767
784
  function applySessionDeletedEvent(event) {
768
785
  busy = false;
769
786
  attention = false;
787
+ removeShellCalls(sessionIdFromEvent(event));
770
788
  removeChildren(sessionIdFromEvent(event));
771
789
  return "sync";
772
790
  }
791
+ function applyShellStartedEvent(event) {
792
+ const key = shellCallKey(event);
793
+ if (!key)
794
+ return "none";
795
+ activeShellCalls.add(key);
796
+ return "sync";
797
+ }
798
+ function applyShellEndedEvent(event) {
799
+ const key = shellCallKey(event);
800
+ if (!key || !activeShellCalls.delete(key))
801
+ return "none";
802
+ return "sync";
803
+ }
804
+ function removeShellCalls(sessionId) {
805
+ if (!sessionId)
806
+ return;
807
+ for (const key of activeShellCalls) {
808
+ if (key.startsWith(`${sessionId}:`))
809
+ activeShellCalls.delete(key);
810
+ }
811
+ }
773
812
  function hasActiveChildren(parentId) {
774
813
  for (const childId of activeChildSessions) {
775
814
  if (!parentId || childSessions.get(childId) === parentId) {
@@ -985,6 +1024,10 @@ function sessionIdFromEvent(event) {
985
1024
  }
986
1025
  return;
987
1026
  }
1027
+ function shellCallKey(event) {
1028
+ const properties = event.properties;
1029
+ return isRecord(properties) && typeof properties.sessionID === "string" && typeof properties.callID === "string" ? `${properties.sessionID}:${properties.callID}` : undefined;
1030
+ }
988
1031
  function assistantTextPart(part) {
989
1032
  const partType = typeof part.type === "string" ? part.type : undefined;
990
1033
  if (partType !== "text" && partType !== "reasoning")
@@ -1,15 +1,900 @@
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
+ var ACCOUNT_PROVIDERS = {
293
+ anthropic: { title: "Anthropic", modelsDevIds: ["anthropic"] },
294
+ openai: { title: "OpenAI", modelsDevIds: ["openai"] },
295
+ cursor: { title: "Cursor" },
296
+ "github-copilot": { title: "GitHub Copilot" },
297
+ xai: { title: "xAI", dashboardUrl: "https://console.x.ai", modelsDevIds: ["xai"] },
298
+ jetbrains: { title: "JetBrains" },
299
+ "opencode-go": { title: "OpenCode Go", modelsDevIds: ["opencode-go"] },
300
+ "opencode-zen": {
301
+ title: "OpenCode Zen",
302
+ dashboardUrl: "https://opencode.ai/auth",
303
+ modelsDevIds: ["opencode"]
304
+ },
305
+ moonshot: { title: "Moonshot", modelsDevIds: ["kimi-code-plan-global", "kimi-code-plan-cn"] },
306
+ "moonshot-platform": {
307
+ title: "Moonshot AI Platform",
308
+ dashboardUrl: "https://platform.moonshot.ai/console/api-keys",
309
+ modelsDevIds: ["moonshotai", "moonshotai-cn"]
310
+ },
311
+ google: {
312
+ title: "Google Gemini API",
313
+ dashboardUrl: "https://aistudio.google.com/apikey",
314
+ modelsDevIds: ["google"]
315
+ },
316
+ openrouter: {
317
+ title: "OpenRouter",
318
+ dashboardUrl: "https://openrouter.ai/settings/keys",
319
+ modelsDevIds: ["openrouter"]
320
+ },
321
+ deepseek: {
322
+ title: "DeepSeek",
323
+ dashboardUrl: "https://platform.deepseek.com/api_keys",
324
+ modelsDevIds: ["deepseek"]
325
+ },
326
+ groq: { title: "Groq", dashboardUrl: "https://console.groq.com/keys", modelsDevIds: ["groq"] },
327
+ mistral: {
328
+ title: "Mistral",
329
+ dashboardUrl: "https://console.mistral.ai/api-keys",
330
+ modelsDevIds: ["mistral"]
331
+ },
332
+ cerebras: {
333
+ title: "Cerebras",
334
+ dashboardUrl: "https://cloud.cerebras.ai",
335
+ modelsDevIds: ["cerebras"]
336
+ },
337
+ fireworks: {
338
+ title: "Fireworks AI",
339
+ dashboardUrl: "https://fireworks.ai/account/api-keys",
340
+ modelsDevIds: ["fireworks-ai"]
341
+ },
342
+ together: {
343
+ title: "Together AI",
344
+ dashboardUrl: "https://api.together.ai/settings/api-keys",
345
+ modelsDevIds: ["togetherai"]
346
+ },
347
+ huggingface: {
348
+ title: "Hugging Face",
349
+ dashboardUrl: "https://huggingface.co/settings/tokens",
350
+ modelsDevIds: ["huggingface"]
351
+ },
352
+ nvidia: { title: "NVIDIA", dashboardUrl: "https://build.nvidia.com", modelsDevIds: ["nvidia"] },
353
+ "vercel-ai-gateway": {
354
+ title: "Vercel AI Gateway",
355
+ dashboardUrl: "https://vercel.com/dashboard/ai-gateway",
356
+ modelsDevIds: ["vercel"]
357
+ },
358
+ zai: {
359
+ title: "Z.ai",
360
+ dashboardUrl: "https://z.ai/manage-apikey/apikey-list",
361
+ modelsDevIds: ["zai-coding-plan", "zai", "zhipuai"]
362
+ },
363
+ minimax: {
364
+ title: "MiniMax",
365
+ dashboardUrl: "https://platform.minimax.io",
366
+ modelsDevIds: ["minimax-coding-plan", "minimax", "minimax-cn-coding-plan", "minimax-cn"]
367
+ },
368
+ xiaomi: {
369
+ title: "Xiaomi MiMo",
370
+ dashboardUrl: "https://platform.xiaomimimo.com",
371
+ modelsDevIds: [
372
+ "xiaomi",
373
+ "xiaomi-token-plan-ams",
374
+ "xiaomi-token-plan-sgp",
375
+ "xiaomi-token-plan-cn"
376
+ ]
377
+ }
378
+ };
379
+ function modelsDevAccountProviders(except = []) {
380
+ return Object.keys(ACCOUNT_PROVIDERS).flatMap((provider) => {
381
+ const { modelsDevIds } = ACCOUNT_PROVIDERS[provider];
382
+ return modelsDevIds && !except.includes(provider) ? [{ provider, modelsDevIds }] : [];
383
+ });
384
+ }
385
+ function apiKeyTokenKind(provider) {
386
+ return `key:${provider}`;
387
+ }
388
+ function defineAccounts(providers) {
389
+ return providers;
390
+ }
391
+ function accountProviderTitle(provider, title) {
392
+ if (title)
393
+ return title;
394
+ return wellKnownProvider(provider)?.title ?? provider;
395
+ }
396
+ function wellKnownProvider(provider) {
397
+ return Object.hasOwn(ACCOUNT_PROVIDERS, provider) ? ACCOUNT_PROVIDERS[provider] : undefined;
398
+ }
399
+ function hostProcess() {
400
+ return globalThis.process;
401
+ }
402
+ function hostBuiltin(id) {
403
+ const load = hostProcess()?.getBuiltinModule;
404
+ if (!load)
405
+ throw new Error(`${id} is only available on the host`);
406
+ return load(id);
407
+ }
408
+ async function expandHome(path) {
409
+ if (path !== "~" && !path.startsWith("~/"))
410
+ return path;
411
+ const { homedir } = hostBuiltin("node:os");
412
+ return `${homedir()}${path.slice(1)}`;
413
+ }
414
+ function recordValue(value) {
415
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
416
+ }
417
+ var STATE_SUFFIX = ".pragma.json";
418
+ var LOCK_SUFFIX = ".lock";
419
+ var LOCK_RETRIES = 50;
420
+ var LOCK_RETRY_MS = 100;
421
+ var LOCK_STALE_MS = 30000;
422
+ var PRIVATE_FILE_MODE = 384;
423
+ async function sharedCredentialFile(store, env) {
424
+ const dir = env[store.dirEnv] ?? hostProcess()?.env?.[store.dirEnv] ?? store.defaultDir;
425
+ return `${await expandHome(dir)}/${store.file}`;
426
+ }
427
+ function loginCredentialFile(store, home) {
428
+ return `${home}/${store.file}`;
429
+ }
430
+ async function readLoginEntries(store, provider, entries, account) {
431
+ const shared = await sharedCredentialFile(store, account.env);
432
+ const state2 = await readState(shared);
433
+ await settleSwitches(store, shared, state2);
434
+ const slot = state2.providers[provider];
435
+ if (holdsShared(slot, account.home)) {
436
+ const current = await readJson(shared);
437
+ return current ? pick(current, entries) : null;
438
+ }
439
+ if (account.home === null)
440
+ return slot?.own ?? {};
441
+ const own = await readJson(loginCredentialFile(store, account.home));
442
+ return own ? pick(own, entries) : null;
443
+ }
444
+ async function writeLoginEntries(store, provider, account, updates) {
445
+ const shared = await sharedCredentialFile(store, account.env);
446
+ const fs = hostBuiltin("node:fs/promises");
447
+ await fs.mkdir(parentDir(shared), { recursive: true, mode: 448 });
448
+ await withLock(fs, shared, async () => {
449
+ const state2 = await readState(shared);
450
+ const settled = await settleSwitches(store, shared, state2);
451
+ const slot = state2.providers[provider];
452
+ let ownChanged = false;
453
+ if (holdsShared(slot, account.home)) {
454
+ await mergeJson(fs, shared, updates);
455
+ } else if (account.home !== null) {
456
+ await mergeJson(fs, loginCredentialFile(store, account.home), updates);
457
+ } else if (slot) {
458
+ slot.own = { ...slot.own, ...updates };
459
+ ownChanged = true;
460
+ }
461
+ if (settled || ownChanged)
462
+ await writeJson(fs, `${shared}${STATE_SUFFIX}`, state2);
463
+ });
464
+ }
465
+ function holdsShared(slot, home) {
466
+ return !slot?.unattributed && (slot?.active ?? null) === home;
467
+ }
468
+ async function mergeJson(fs, path, updates) {
469
+ await fs.mkdir(parentDir(path), { recursive: true, mode: 448 });
470
+ await writeJson(fs, path, { ...await readJson(path), ...updates });
471
+ }
472
+ async function activateCredentialSwap(store, provider, entries, account) {
473
+ const shared = await sharedCredentialFile(store, account.env);
474
+ const fs = hostBuiltin("node:fs/promises");
475
+ await fs.mkdir(parentDir(shared), { recursive: true, mode: 448 });
476
+ await withLock(fs, shared, () => swapLocked({ fs, store, shared, provider, entries, target: account.home }));
477
+ }
478
+ async function swapLocked(swap) {
479
+ const { fs, store, shared, provider, entries, target } = swap;
480
+ const stateFile = `${shared}${STATE_SUFFIX}`;
481
+ const state2 = await readState(shared);
482
+ const settled = await settleSwitches(store, shared, state2);
483
+ const slot = state2.providers[provider] ?? { active: null };
484
+ if (slot.active === target && !slot.unattributed) {
485
+ if (settled)
486
+ await writeJson(fs, stateFile, state2);
487
+ return;
488
+ }
489
+ const current = await readJson(shared) ?? {};
490
+ const outgoing = pick(current, entries);
491
+ const incoming = await savedEntries(store, slot, target, entries);
492
+ const own = await saveOutgoing(swap, slot, outgoing);
493
+ state2.providers[provider] = {
494
+ active: slot.active,
495
+ ...own ? { own } : {},
496
+ switching: { target, outgoing, entries: [...entries] }
497
+ };
498
+ await writeJson(fs, stateFile, state2);
499
+ await writeJson(fs, shared, { ...omit(current, entries), ...incoming });
500
+ completeSwitch(state2, provider, target, false);
501
+ await writeJson(fs, stateFile, state2);
502
+ }
503
+ async function savedEntries(store, slot, target, entries) {
504
+ if (target === null)
505
+ return slot.own ?? {};
506
+ const stored = await readJson(loginCredentialFile(store, target));
507
+ return pick(stored ?? {}, entries);
508
+ }
509
+ async function settleSwitches(store, shared, state2) {
510
+ let changed = false;
511
+ let current;
512
+ for (const [provider, slot] of Object.entries(state2.providers)) {
513
+ const pending = slot.switching;
514
+ if (!pending)
515
+ continue;
516
+ changed = true;
517
+ current ??= await readJson(shared) ?? {};
518
+ const inShared = pick(current, pending.entries);
519
+ const incoming = await savedEntries(store, slot, pending.target, pending.entries);
520
+ if (sameEntries(inShared, incoming)) {
521
+ completeSwitch(state2, provider, pending.target, false);
522
+ } else if (sameEntries(inShared, pending.outgoing)) {
523
+ delete slot.switching;
524
+ } else {
525
+ completeSwitch(state2, provider, pending.target, true);
526
+ }
527
+ }
528
+ return changed;
529
+ }
530
+ function completeSwitch(state2, provider, target, unattributed) {
531
+ const own = state2.providers[provider]?.own;
532
+ if (target === null && !unattributed) {
533
+ delete state2.providers[provider];
534
+ return;
535
+ }
536
+ state2.providers[provider] = {
537
+ active: target,
538
+ ...own ? { own } : {},
539
+ ...unattributed ? { unattributed } : {}
540
+ };
541
+ }
542
+ async function saveOutgoing(swap, slot, outgoing) {
543
+ if (slot.unattributed)
544
+ return slot.own;
545
+ if (slot.active !== null) {
546
+ await saveBack(swap.fs, loginCredentialFile(swap.store, slot.active), outgoing, swap.entries);
547
+ return slot.own;
548
+ }
549
+ return outgoing;
550
+ }
551
+ async function saveBack(fs, path, outgoing, entries) {
552
+ const existing = await readJson(path);
553
+ if (existing === null) {
554
+ try {
555
+ await fs.stat(parentDir(path));
556
+ } catch {
557
+ return;
558
+ }
559
+ }
560
+ await writeJson(fs, path, { ...omit(existing ?? {}, entries), ...outgoing });
561
+ }
562
+ async function withLock(fs, file, run) {
563
+ const lock = `${file}${LOCK_SUFFIX}`;
564
+ for (let attempt = 0;; attempt += 1) {
565
+ try {
566
+ await fs.mkdir(lock);
567
+ break;
568
+ } catch (error) {
569
+ if (!isExists(error) || attempt >= LOCK_RETRIES)
570
+ throw error;
571
+ if (await isStale(fs, lock)) {
572
+ await fs.rmdir(lock).catch(() => {
573
+ return;
574
+ });
575
+ continue;
576
+ }
577
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
578
+ }
579
+ }
580
+ try {
581
+ return await run();
582
+ } finally {
583
+ await fs.rmdir(lock).catch(() => {
584
+ return;
585
+ });
586
+ }
587
+ }
588
+ async function isStale(fs, lock) {
589
+ try {
590
+ return Date.now() - (await fs.stat(lock)).mtimeMs > LOCK_STALE_MS;
591
+ } catch {
592
+ return false;
593
+ }
594
+ }
595
+ function isExists(error) {
596
+ return recordValue(error)?.code === "EEXIST";
597
+ }
598
+ async function readState(shared) {
599
+ const value = await readJson(`${shared}${STATE_SUFFIX}`);
600
+ const providers = recordValue(value?.providers);
601
+ return { providers: providers ?? {} };
602
+ }
603
+ async function readJson(path) {
604
+ const { readFile } = hostBuiltin("node:fs/promises");
605
+ try {
606
+ return recordValue(JSON.parse(await readFile(path, "utf8"))) ?? null;
607
+ } catch {
608
+ return null;
609
+ }
610
+ }
611
+ async function writeJson(fs, path, value) {
612
+ await fs.writeFile(path, `${JSON.stringify(value, null, 2)}
613
+ `, { mode: PRIVATE_FILE_MODE });
614
+ await fs.chmod(path, PRIVATE_FILE_MODE);
615
+ }
616
+ function sameEntries(a, b) {
617
+ return stableJson(a) === stableJson(b);
618
+ }
619
+ function stableJson(value) {
620
+ if (Array.isArray(value))
621
+ return `[${value.map(stableJson).join(",")}]`;
622
+ const record2 = recordValue(value);
623
+ if (!record2)
624
+ return JSON.stringify(value) ?? "null";
625
+ const keys = Object.keys(record2).toSorted();
626
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(record2[key])}`).join(",")}}`;
627
+ }
628
+ function pick(source, keys) {
629
+ return Object.fromEntries(keys.filter((key) => (key in source)).map((key) => [key, source[key]]));
630
+ }
631
+ function omit(source, keys) {
632
+ return Object.fromEntries(Object.entries(source).filter(([key]) => !keys.includes(key)));
633
+ }
634
+ function parentDir(path) {
635
+ const index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
636
+ return index > 0 ? path.slice(0, index) : path;
637
+ }
638
+ var CHATGPT_PROFILE_CLAIM = "https://api.openai.com/profile";
639
+ var CHATGPT_AUTH_CLAIM = "https://api.openai.com/auth";
640
+ var ANTHROPIC_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
641
+ var ANTHROPIC_OAUTH_BETA = "oauth-2025-04-20";
642
+ var GITHUB_USER_URL = "https://api.github.com/user";
643
+ var API_KEY_DIGEST_LENGTH = 16;
644
+ function chatGptIdentity(accessToken) {
645
+ const claims = decodeJwtPayload(accessToken);
646
+ if (!claims)
647
+ return null;
648
+ const profile = recordValue(claims[CHATGPT_PROFILE_CLAIM]);
649
+ const auth = recordValue(claims[CHATGPT_AUTH_CLAIM]);
650
+ const email = stringValue(profile?.email);
651
+ if (!email)
652
+ return null;
653
+ const name = stringValue(profile?.name);
654
+ const plan = stringValue(auth?.chatgpt_plan_type);
655
+ return {
656
+ id: email,
657
+ email,
658
+ ...name ? { name } : {},
659
+ ...plan ? { plan: capitalize(plan) } : {}
660
+ };
661
+ }
662
+ async function anthropicOAuthIdentity(accessToken, fetcher = fetch) {
663
+ const response = await fetcher(ANTHROPIC_PROFILE_URL, {
664
+ headers: {
665
+ Authorization: `Bearer ${accessToken}`,
666
+ "anthropic-beta": ANTHROPIC_OAUTH_BETA
667
+ }
668
+ });
669
+ if (!response.ok)
670
+ return null;
671
+ return parseAnthropicProfile(await response.json());
672
+ }
673
+ function parseAnthropicProfile(value) {
674
+ const account = recordValue(recordValue(value)?.account);
675
+ const organization = recordValue(recordValue(value)?.organization);
676
+ const email = stringValue(account?.email);
677
+ const orgId = stringValue(organization?.uuid);
678
+ const id = orgId && email ? `${orgId}:${email}` : orgId ?? email;
679
+ if (!id)
680
+ return null;
681
+ const name = stringValue(organization?.name);
682
+ return { id, ...email ? { email } : {}, ...name ? { name } : {} };
683
+ }
684
+ async function gitHubIdentity(token, fetcher = fetch) {
685
+ const response = await fetcher(GITHUB_USER_URL, {
686
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" }
687
+ });
688
+ if (!response.ok)
689
+ return null;
690
+ const user = recordValue(await response.json());
691
+ const login = stringValue(user?.login);
692
+ if (!login)
693
+ return null;
694
+ const name = stringValue(user?.name);
695
+ const email = stringValue(user?.email);
696
+ return { id: login.toLowerCase(), ...name ? { name } : {}, ...email ? { email } : {} };
697
+ }
698
+ async function apiKeyIdentity(key) {
699
+ const trimmed = key.trim();
700
+ if (!trimmed)
701
+ return null;
702
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(trimmed));
703
+ const hex = [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, API_KEY_DIGEST_LENGTH);
704
+ return { id: `key:${hex}`, name: `API key …${trimmed.slice(-4)}` };
705
+ }
706
+ async function credentialDir(ctx, envVar, fallback) {
707
+ const configured = ctx.account.env[envVar] ?? hostProcess()?.env?.[envVar];
708
+ if (configured)
709
+ return configured;
710
+ return expandHome(fallback);
711
+ }
712
+ async function readCredentialFile(path) {
713
+ const { readFile } = hostBuiltin("node:fs/promises");
714
+ try {
715
+ return recordValue(JSON.parse(await readFile(await expandHome(path), "utf8"))) ?? null;
716
+ } catch {
717
+ return null;
718
+ }
719
+ }
720
+ async function writeCredentialFile(path, value) {
721
+ const fs = hostBuiltin("node:fs/promises");
722
+ const target = await expandHome(path);
723
+ const slash = Math.max(target.lastIndexOf("/"), target.lastIndexOf("\\"));
724
+ if (slash > 0)
725
+ await fs.mkdir(target.slice(0, slash), { recursive: true, mode: 448 });
726
+ await fs.writeFile(target, `${JSON.stringify(value, null, 2)}
727
+ `, { mode: 384 });
728
+ await fs.chmod(target, 384);
729
+ }
730
+ function credentialStorePath(store, home) {
731
+ return `${home ?? `$${store.dirEnv} or ${store.defaultDir}`}/${store.file}`;
732
+ }
733
+ function identifyFromCredentialStore(store, entryKey, provider, options = {}) {
734
+ const entryKeys = typeof entryKey === "string" ? [entryKey] : entryKey;
735
+ return async (ctx) => {
736
+ const dir = await credentialDir(ctx, store.dirEnv, store.defaultDir);
737
+ const credentials = await readCredentialFile(`${dir}/${store.file}`);
738
+ return identifyEntries(provider, credentials, entryKeys, options.apiKeyOnly ?? false);
739
+ };
740
+ }
741
+ function credentialStoreAccount(options) {
742
+ const { store, entries, provider } = options;
743
+ const apiKeyOnly = options.apiKeyOnly ?? false;
744
+ const shared = sharedTokenOptions(options);
745
+ const base = {
746
+ provider,
747
+ agent: options.agent,
748
+ ...options.login ? { login: options.login } : {},
749
+ credentialPath: (home) => home === null ? credentialStorePath(store, null) : loginCredentialFile(store, home)
750
+ };
751
+ if (!(options.switchable ?? shared !== null)) {
752
+ return {
753
+ ...base,
754
+ identify: identifyFromCredentialStore(store, entries, provider, { apiKeyOnly })
755
+ };
756
+ }
757
+ return {
758
+ ...base,
759
+ swap: {
760
+ activate: (ctx) => activateCredentialSwap(store, provider, entries, ctx.account)
761
+ },
762
+ identify: async (ctx) => {
763
+ const stored = await readLoginEntries(store, provider, entries, ctx.account);
764
+ return identifyEntries(provider, stored, entries, apiKeyOnly);
765
+ },
766
+ ...shared ? {
767
+ sharedToken: {
768
+ kind: shared.kind,
769
+ read: async (ctx) => {
770
+ const stored = await readLoginEntries(store, provider, entries, ctx.account);
771
+ const keys = options.sharedToken ? [shared.entry] : entries;
772
+ for (const key of keys) {
773
+ const token = entryToken(stored?.[key], shared.type);
774
+ if (token)
775
+ return token;
776
+ }
777
+ return null;
778
+ },
779
+ write: (ctx, token) => writeLoginEntries(store, provider, ctx.account, {
780
+ [shared.entry]: tokenEntry(token, shared.type, store)
781
+ })
782
+ }
783
+ } : {}
784
+ };
785
+ }
786
+ function sharedTokenOptions(options) {
787
+ if (options.sharedToken === false)
788
+ return null;
789
+ const shared = options.sharedToken ?? {
790
+ kind: apiKeyTokenKind(options.provider),
791
+ entry: options.entries[0] ?? options.provider,
792
+ type: "api"
793
+ };
794
+ return shared.type === "oauth" && options.apiKeyOnly ? null : shared;
795
+ }
796
+ function entryToken(entry, type) {
797
+ const credential = recordValue(entry);
798
+ if (!credential)
799
+ return null;
800
+ if (type === "api") {
801
+ if (credential.type === "oauth")
802
+ return null;
803
+ const key = stringValue(credential.key);
804
+ return key ? { type: "api", key } : null;
805
+ }
806
+ return oauthEntryToken(credential);
807
+ }
808
+ function tokenEntry(token, type, store) {
809
+ if (token.type !== type) {
810
+ throw new Error(`expected a shared ${type} token, got ${token.type}`);
811
+ }
812
+ if (token.type === "api")
813
+ return { type: store.apiKeyType ?? "api", key: token.key };
814
+ return {
815
+ type: "oauth",
816
+ access: token.access,
817
+ refresh: token.refresh,
818
+ expires: token.expires,
819
+ ...token.accountId ? { accountId: token.accountId } : {}
820
+ };
821
+ }
822
+ function credentialFileSharedToken(store, shared) {
823
+ const path = async (ctx) => ctx.account.home === null ? `${await credentialDir(ctx, store.dirEnv, store.defaultDir)}/${store.file}` : loginCredentialFile(store, ctx.account.home);
824
+ return {
825
+ kind: shared.kind,
826
+ read: async (ctx) => entryToken((await readCredentialFile(await path(ctx)))?.[shared.entry], shared.type),
827
+ write: async (ctx, token) => {
828
+ const file = await path(ctx);
829
+ const current = await readCredentialFile(file) ?? {};
830
+ await writeCredentialFile(file, {
831
+ ...current,
832
+ [shared.entry]: tokenEntry(token, shared.type, store)
833
+ });
834
+ }
835
+ };
836
+ }
837
+ function oauthEntryToken(entry) {
838
+ const credential = recordValue(entry);
839
+ if (credential?.type !== "oauth")
840
+ return null;
841
+ const access = stringValue(credential.access);
842
+ const refresh = stringValue(credential.refresh);
843
+ if (!access || !refresh)
844
+ return null;
845
+ const expires = typeof credential.expires === "number" ? credential.expires : 0;
846
+ const accountId = stringValue(credential.accountId);
847
+ return { type: "oauth", access, refresh, expires, ...accountId ? { accountId } : {} };
848
+ }
849
+ async function identifyEntries(provider, stored, entries, apiKeyOnly) {
850
+ for (const key of entries) {
851
+ const entry = stored?.[key];
852
+ if (apiKeyOnly && recordValue(entry)?.type === "oauth")
853
+ continue;
854
+ const identity = await storedCredentialIdentity(provider, entry);
855
+ if (identity)
856
+ return identity;
857
+ }
858
+ return null;
859
+ }
860
+ async function storedCredentialIdentity(provider, entry, fetcher = fetch) {
861
+ const credential = recordValue(entry);
862
+ if (!credential)
863
+ return null;
864
+ const key = stringValue(credential.key);
865
+ if (credential.type !== "oauth")
866
+ return key ? apiKeyIdentity(key) : null;
867
+ const access = stringValue(credential.access);
868
+ const refresh = stringValue(credential.refresh);
869
+ switch (provider) {
870
+ case "openai":
871
+ return access ? chatGptIdentity(access) : null;
872
+ case "anthropic":
873
+ return access ? anthropicOAuthIdentity(access, fetcher) : null;
874
+ case "github-copilot":
875
+ return refresh && !credential.enterpriseUrl ? gitHubIdentity(refresh, fetcher) : null;
876
+ default:
877
+ return null;
878
+ }
879
+ }
880
+ function decodeJwtPayload(token) {
881
+ const payload = token.split(".")[1];
882
+ if (!payload)
883
+ return null;
884
+ try {
885
+ const base64 = payload.replaceAll("-", "+").replaceAll("_", "/");
886
+ const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
887
+ const bytes = Uint8Array.from(atob(padded), (char) => char.charCodeAt(0));
888
+ return recordValue(JSON.parse(new TextDecoder().decode(bytes))) ?? null;
889
+ } catch {
890
+ return null;
891
+ }
892
+ }
893
+ function capitalize(value) {
894
+ return value.charAt(0).toUpperCase() + value.slice(1);
895
+ }
896
+ function stringValue(value) {
897
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
13
898
  }
14
899
 
15
900
  // ../watcher-kit/src/index.ts
@@ -210,8 +1095,8 @@ async function finalizeQuestionAnswers(ctx, runtime) {
210
1095
  if (!ctx.signal.aborted)
211
1096
  await writeKeys(ctx, runtime.questionFinalizeKeys);
212
1097
  }
213
- async function handleInterjection(ctx, submitKeys, submitDelayMs, mode, text) {
214
- const input = mode === "plain" ? text : `${BRACKETED_PASTE_START}${text}${BRACKETED_PASTE_END}`;
1098
+ async function handleInterjection(ctx, submitKeys, submitDelayMs, mode, text2) {
1099
+ const input = mode === "plain" ? text2 : `${BRACKETED_PASTE_START}${text2}${BRACKETED_PASTE_END}`;
215
1100
  await writeKeys(ctx, input);
216
1101
  if (!submitKeys) {
217
1102
  return;
@@ -229,15 +1114,15 @@ function rememberCommandAttention(runtime, event) {
229
1114
  runtime.outstandingCommandRequestId = null;
230
1115
  }
231
1116
  }
232
- function rememberQuestion(cache, event) {
1117
+ function rememberQuestion(cache2, event) {
233
1118
  if (event.status === "attention" && event.attentionKind === "question" && typeof event.requestId === "string" && event.requestId.length > 0) {
234
1119
  const entries = (event.questions ?? []).flatMap(questionEntry);
235
1120
  if (entries.length > 0) {
236
- cache.set(event.requestId, { questions: entries });
1121
+ cache2.set(event.requestId, { questions: entries });
237
1122
  return;
238
1123
  }
239
1124
  const options = (event.options ?? []).map((option) => option.label).filter((option) => option.trim() !== "");
240
- cache.set(event.requestId, {
1125
+ cache2.set(event.requestId, {
241
1126
  questions: [{ question: event.question ?? "", options }]
242
1127
  });
243
1128
  }
@@ -246,12 +1131,12 @@ function questionEntry(value) {
246
1131
  if (typeof value !== "object" || value === null || !("question" in value)) {
247
1132
  return [];
248
1133
  }
249
- const record = value;
250
- if (typeof record.question !== "string" || !record.question.trim()) {
1134
+ const record2 = value;
1135
+ if (typeof record2.question !== "string" || !record2.question.trim()) {
251
1136
  return [];
252
1137
  }
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 }];
1138
+ 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() !== "") : [];
1139
+ return [{ question: record2.question, options }];
255
1140
  }
256
1141
  function questionAnswerKeys(input) {
257
1142
  if (input.dismissed || input.reply === null) {
@@ -347,7 +1232,7 @@ SELECT
347
1232
  FROM usage
348
1233
  `.trim();
349
1234
  function usageCommand() {
350
- return globalThis.process?.platform === "win32" ? `if (-not (Get-Command opencode -ErrorAction SilentlyContinue)) { exit 20 }; opencode db ${powershellQuote(USAGE_QUERY)} --format json` : "command -v opencode >/dev/null 2>&1 || exit 20; " + `opencode db ${shellQuote(USAGE_QUERY)} --format json`;
1235
+ return globalThis.process?.platform === "win32" ? `if (-not (Get-Command opencode -ErrorAction SilentlyContinue)) { exit 20 }; opencode db ${powershellQuote(USAGE_QUERY)} --format json` : "command -v opencode >/dev/null 2>&1 || exit 20; " + `opencode db ${shellQuote3(USAGE_QUERY)} --format json`;
351
1236
  }
352
1237
  async function loadOpenCodeGoUsageLimits(ctx) {
353
1238
  const [result] = await ctx.sdk.exec.run({
@@ -420,7 +1305,7 @@ function finiteNumber(value) {
420
1305
  function isRecord(value) {
421
1306
  return typeof value === "object" && value !== null && !Array.isArray(value);
422
1307
  }
423
- function shellQuote(value) {
1308
+ function shellQuote3(value) {
424
1309
  return `'${value.replace(/'/g, `'"'"'`)}'`;
425
1310
  }
426
1311
  function powershellQuote(value) {
@@ -428,7 +1313,47 @@ function powershellQuote(value) {
428
1313
  }
429
1314
 
430
1315
  // src/pragma-plugin.ts
1316
+ var OPENCODE_STORE = {
1317
+ dirEnv: "XDG_DATA_HOME",
1318
+ defaultDir: "~/.local/share",
1319
+ file: "opencode/auth.json"
1320
+ };
1321
+ var OPENCODE_API_KEY_PROVIDERS = modelsDevAccountProviders([
1322
+ "openai",
1323
+ "opencode-go"
1324
+ ]);
1325
+ function openCodeApiKeyProvider({
1326
+ provider,
1327
+ modelsDevIds
1328
+ }) {
1329
+ return credentialStoreAccount({
1330
+ provider,
1331
+ agent: "opencode",
1332
+ store: OPENCODE_STORE,
1333
+ entries: modelsDevIds,
1334
+ apiKeyOnly: true,
1335
+ login: {
1336
+ command: ["opencode", "auth", "login", "--provider", modelsDevIds[0]],
1337
+ instructions: `Paste your ${accountProviderTitle(provider)} API key here, then send.`
1338
+ }
1339
+ });
1340
+ }
1341
+ var OPENCODE_GITHUB_CLIENT_ID = "Ov23li8tweQw6odWQebz";
431
1342
  var ansiEscapePattern = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");
1343
+ var DEFAULT_MODES = [
1344
+ { id: "build", name: "Build" },
1345
+ { id: "plan", name: "Plan" }
1346
+ ];
1347
+ var HIDDEN_AGENTS = new Set(["compaction", "summary", "title"]);
1348
+ var BUILTIN_SLASH_COMMANDS = [
1349
+ { name: "init", description: "Create or update AGENTS.md" },
1350
+ { name: "review", description: "Review changes", argumentHint: "[commit|branch|pr]" }
1351
+ ];
1352
+ var SLASH_COMMAND_SOURCES = [
1353
+ ...commandAndSkillDirs([".opencode", "~/.config/opencode"]),
1354
+ { dir: ".opencode/command", layout: "files" },
1355
+ { dir: "~/.config/opencode/command", layout: "files" }
1356
+ ];
432
1357
  var opencodeAgentPlugin = definePlugin({
433
1358
  name: "OpenCode",
434
1359
  description: "Launch OpenCode from Pragma.",
@@ -439,16 +1364,74 @@ var opencodeAgentPlugin = definePlugin({
439
1364
  questionFinalizeKeys: "\r"
440
1365
  })
441
1366
  ],
442
- usageLimits: [
443
- defineUsageLimitProvider({
444
- id: "opencode-go",
445
- title: "OpenCode Go",
1367
+ accounts: defineAccounts([
1368
+ {
1369
+ provider: "opencode-go",
1370
+ agent: "opencode",
446
1371
  dashboardUrl: "https://opencode.ai/auth",
447
1372
  iconPath: "assets/opencode.svg",
448
- primaryLimitId: "rolling",
449
- load: loadOpenCodeGoUsageLimits
450
- })
451
- ],
1373
+ login: {
1374
+ command: ["opencode", "auth", "login"],
1375
+ instructions: "Pick OpenCode Go when asked (type to filter, then send), then paste the key from opencode.ai/auth."
1376
+ },
1377
+ env: (home) => ({ XDG_DATA_HOME: home }),
1378
+ credentialPath: (home) => credentialStorePath(OPENCODE_STORE, home),
1379
+ identify: identifyFromCredentialStore(OPENCODE_STORE, "opencode-go", "opencode-go"),
1380
+ sharedToken: credentialFileSharedToken(OPENCODE_STORE, {
1381
+ kind: apiKeyTokenKind("opencode-go"),
1382
+ entry: "opencode-go",
1383
+ type: "api"
1384
+ }),
1385
+ usageLimits: {
1386
+ primaryLimitId: "rolling",
1387
+ load: loadOpenCodeGoUsageLimits
1388
+ }
1389
+ },
1390
+ {
1391
+ ...credentialStoreAccount({
1392
+ provider: "openai",
1393
+ agent: "opencode",
1394
+ store: OPENCODE_STORE,
1395
+ entries: ["openai"],
1396
+ switchable: true,
1397
+ sharedToken: { kind: "chatgpt", entry: "openai", type: "oauth" },
1398
+ login: {
1399
+ command: [
1400
+ "opencode",
1401
+ "auth",
1402
+ "login",
1403
+ "--provider",
1404
+ "openai",
1405
+ "--method",
1406
+ "ChatGPT Pro/Plus (browser)"
1407
+ ],
1408
+ instructions: "Sign in with ChatGPT in the browser tab that opens."
1409
+ }
1410
+ }),
1411
+ dashboardUrl: "https://chatgpt.com/codex/settings/usage"
1412
+ },
1413
+ {
1414
+ ...credentialStoreAccount({
1415
+ provider: "github-copilot",
1416
+ agent: "opencode",
1417
+ store: OPENCODE_STORE,
1418
+ entries: ["github-copilot"],
1419
+ switchable: true,
1420
+ sharedToken: {
1421
+ kind: `github-copilot:${OPENCODE_GITHUB_CLIENT_ID}`,
1422
+ entry: "github-copilot",
1423
+ type: "oauth"
1424
+ },
1425
+ login: {
1426
+ command: ["opencode", "auth", "login", "--provider", "github-copilot"],
1427
+ input: [""],
1428
+ instructions: "Enter the code shown here on the GitHub page that opens."
1429
+ }
1430
+ }),
1431
+ dashboardUrl: "https://github.com/settings/copilot"
1432
+ },
1433
+ ...OPENCODE_API_KEY_PROVIDERS.map(openCodeApiKeyProvider)
1434
+ ]),
452
1435
  agents: [
453
1436
  defineAgent({
454
1437
  id: "opencode",
@@ -459,10 +1442,15 @@ var opencodeAgentPlugin = definePlugin({
459
1442
  prefillDelayMs: 6000,
460
1443
  models: async (ctx) => parseOpenCodeModels(await execFirst(ctx, "opencode models --json 2>/dev/null || opencode models --verbose 2>/dev/null || opencode models 2>/dev/null")),
461
1444
  permissionModes: [],
1445
+ modes: async (ctx) => parseOpenCodeAgents(await execFirst(ctx, "opencode agent list 2>/dev/null")),
1446
+ slashCommands: slashCommandProvider(BUILTIN_SLASH_COMMANDS, SLASH_COMMAND_SOURCES, {
1447
+ acp: { command: ["opencode", "acp"] }
1448
+ }),
462
1449
  args: {
463
1450
  model: (modelId) => ["--model", modelId],
464
1451
  reasoning: () => [],
465
- permissionMode: () => []
1452
+ permissionMode: () => [],
1453
+ mode: (modeId) => ["--agent", modeId]
466
1454
  }
467
1455
  })
468
1456
  ]
@@ -472,6 +1460,20 @@ async function execFirst(ctx, command) {
472
1460
  const [result] = await ctx.sdk.exec.run({ cwd: pluginCwd(ctx), commands: [command] });
473
1461
  return result?.stdout ?? "";
474
1462
  }
1463
+ function parseOpenCodeAgents(output) {
1464
+ const names = output.replaceAll(ansiEscapePattern, "").split(`
1465
+ `).flatMap((line) => {
1466
+ const match = /^(\S+) \((primary|all|subagent)\)$/.exec(line.trim());
1467
+ return match && match[2] !== "subagent" && !HIDDEN_AGENTS.has(match[1]) ? [match[1]] : [];
1468
+ });
1469
+ if (names.length === 0)
1470
+ return DEFAULT_MODES;
1471
+ const rank = (name) => {
1472
+ const index = DEFAULT_MODES.findIndex((mode) => mode.id === name);
1473
+ return index === -1 ? DEFAULT_MODES.length : index;
1474
+ };
1475
+ return [...new Set(names)].toSorted((a, b) => rank(a) - rank(b)).map((id) => ({ id, name: id[0].toUpperCase() + id.slice(1) }));
1476
+ }
475
1477
  function parseOpenCodeModels(output) {
476
1478
  const jsonModels = parseJsonModels(output);
477
1479
  if (jsonModels.length > 0) {
@@ -534,27 +1536,27 @@ function walk(value, models) {
534
1536
  function isRecord2(value) {
535
1537
  return Boolean(value) && typeof value === "object";
536
1538
  }
537
- function modelFromRecord(record) {
538
- const id = firstString(record.id, record.model);
539
- const name = modelName(record, id);
540
- if (!id || !name || !hasModelMetadata(record)) {
1539
+ function modelFromRecord(record2) {
1540
+ const id = firstString(record2.id, record2.model);
1541
+ const name = modelName(record2, id);
1542
+ if (!id || !name || !hasModelMetadata(record2)) {
541
1543
  return null;
542
1544
  }
543
- return { id, name: withProvider(id, name, stringValue(record.provider)) };
1545
+ return { id, name: withProvider(id, name, stringValue2(record2.provider)) };
544
1546
  }
545
1547
  function firstString(...values) {
546
1548
  for (const value of values) {
547
- const string = stringValue(value);
1549
+ const string = stringValue2(value);
548
1550
  if (string)
549
1551
  return string;
550
1552
  }
551
1553
  return;
552
1554
  }
553
- function modelName(record, id) {
554
- return stringValue(record.name) ?? stringValue(record.displayName) ?? stringValue(record.label) ?? id;
1555
+ function modelName(record2, id) {
1556
+ return stringValue2(record2.name) ?? stringValue2(record2.displayName) ?? stringValue2(record2.label) ?? id;
555
1557
  }
556
- function hasModelMetadata(record) {
557
- return Boolean(record.provider || record.cost || record.limits || record.modalities);
1558
+ function hasModelMetadata(record2) {
1559
+ return Boolean(record2.provider || record2.cost || record2.limits || record2.modalities);
558
1560
  }
559
1561
  function uniqueModels(models) {
560
1562
  const seen = new Set;
@@ -565,7 +1567,7 @@ function uniqueModels(models) {
565
1567
  return true;
566
1568
  });
567
1569
  }
568
- function stringValue(value) {
1570
+ function stringValue2(value) {
569
1571
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
570
1572
  }
571
1573
  function splitWhitespaceModel(line) {
@@ -585,6 +1587,8 @@ function withProvider(id, name, provider = id.includes("/") ? id.split("/", 1)[0
585
1587
  }
586
1588
  export {
587
1589
  parseOpenCodeModels,
1590
+ parseOpenCodeAgents,
588
1591
  opencodeAgentPlugin,
589
- pragma_plugin_default as default
1592
+ pragma_plugin_default as default,
1593
+ OPENCODE_API_KEY_PROVIDERS
590
1594
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pragma-sh/opencode-plugin",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "opencode plugin that reports agent status to Pragma automatically.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {