claude-code-session-manager 0.28.0 → 0.30.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.
@@ -20,13 +20,29 @@ const { ipcMain } = require('electron');
20
20
  const pty = require('node-pty');
21
21
  const path = require('node:path');
22
22
  const os = require('node:os');
23
- const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
23
+ const fs = require('node:fs');
24
+ const { cleanChildEnv, pathWithUserBins } = require('./lib/cleanEnv.cjs');
24
25
  const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
25
26
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
26
27
  const { schemas } = require('./ipcSchemas.cjs');
27
28
 
28
29
  const SLUG_RE = /^[a-z0-9\-/]+$/;
30
+ // Marketplace name: same shape as a plugin slug. Marketplace source (`add`):
31
+ // a GitHub `owner/repo` (case-sensitive) or a dotted/hyphenated path segment.
32
+ // Deliberately excludes whitespace, flags (leading `-`), and shell metachars.
33
+ const MKT_NAME_RE = /^[a-z0-9\-/]+$/;
34
+ const MKT_ADD_RE = /^[A-Za-z0-9][A-Za-z0-9._\-]*\/[A-Za-z0-9._\-]+$/;
35
+ // Sentinel: register the marketplace that ships inside this app's own files
36
+ // (the npx distribution). Resolved to an absolute path in-process so the
37
+ // renderer never supplies a filesystem path. Works offline — no GitHub/npm.
38
+ const BUNDLED_ADD = 'bundled';
29
39
  const MAX_LINE_BYTES = 16 * 1024;
40
+
41
+ /** Absolute path to the packaged marketplace root (package dir holding
42
+ * `.claude-plugin/marketplace.json`). src/main/ → ../../ */
43
+ function bundledMarketplaceDir() {
44
+ return path.join(__dirname, '..', '..');
45
+ }
30
46
  const KILL_AFTER_MS = 5 * 60 * 1000; // 5 min hard ceiling per install
31
47
  const KILL_GRACE_MS = 5_000; // SIGTERM → SIGKILL escalation window
32
48
 
@@ -41,52 +57,43 @@ function send(channel, payload) {
41
57
  sendIfAlive(mainWindow, channel, payload);
42
58
  }
43
59
 
44
- function install({ slug }) {
45
- if (typeof slug !== 'string' || !SLUG_RE.test(slug) || slug.length > 128) {
46
- return Promise.resolve({ ok: false, exitCode: -1, error: 'invalid slug' });
47
- }
48
- if (inFlight.has(slug)) {
49
- return Promise.resolve({ ok: false, exitCode: -1, error: 'install already in progress' });
50
- }
60
+ function childEnv() {
61
+ return cleanChildEnv({
62
+ PATH: pathWithUserBins(),
63
+ TERM: 'xterm-256color',
64
+ FORCE_COLOR: '0', // strip ANSI so the renderer doesn't have to.
65
+ });
66
+ }
51
67
 
68
+ /**
69
+ * Run one `claude plugin …` pty step, streaming output under `slug` and
70
+ * resolving with the exit code. `register`/`unregister` thread the live proc
71
+ * into the inFlight map so plugins:abort can kill whichever step is current.
72
+ */
73
+ function runStep({ slug, args, register, unregister }) {
52
74
  return new Promise((resolve) => {
53
- const home = os.homedir();
54
- const extraPath = [
55
- path.join(home, '.local', 'bin'),
56
- path.join(home, '.npm-global', 'bin'),
57
- '/usr/local/bin',
58
- '/usr/bin',
59
- '/bin',
60
- ].join(':');
61
- const env = cleanChildEnv({
62
- PATH: `${extraPath}:${process.env.PATH || ''}`,
63
- TERM: 'xterm-256color',
64
- FORCE_COLOR: '0', // strip ANSI so the renderer doesn't have to.
65
- });
66
-
67
75
  const claudeBin = resolveClaudeBin();
68
76
  let proc;
69
77
  try {
70
- proc = pty.spawn(claudeBin, ['plugin', 'install', slug], {
78
+ proc = pty.spawn(claudeBin, args, {
71
79
  name: 'xterm-256color',
72
80
  cols: 120,
73
81
  rows: 30,
74
- cwd: home,
75
- env,
82
+ cwd: os.homedir(),
83
+ env: childEnv(),
76
84
  });
77
85
  } catch (err) {
78
86
  resolve({ ok: false, exitCode: -1, error: `spawn failed: ${err?.message ?? String(err)}` });
79
87
  return;
80
88
  }
81
89
 
82
- inFlight.set(slug, proc);
90
+ register(proc);
83
91
 
84
92
  let lineBuf = '';
85
93
  let settled = false;
86
94
 
87
95
  const killTimer = setTimeout(() => {
88
96
  try { proc.kill('SIGTERM'); } catch { /* */ }
89
- // Escalate to SIGKILL after KILL_GRACE_MS if the pty hasn't exited.
90
97
  const escalate = setTimeout(() => {
91
98
  try { proc.kill('SIGKILL'); } catch { /* already dead */ }
92
99
  }, KILL_GRACE_MS);
@@ -96,11 +103,11 @@ function install({ slug }) {
96
103
 
97
104
  // Belt-and-suspenders: if onExit never fires (broken pty event path after
98
105
  // SIGKILL — analogous to anthropics/claude-code #61735's unreachable pts),
99
- // force-release the inFlight lock so the slug isn't permanently stuck.
106
+ // force-resolve so the step (and its inFlight lock) isn't permanently stuck.
100
107
  const deadman = setTimeout(() => {
101
108
  if (settled) return;
102
109
  settled = true;
103
- inFlight.delete(slug);
110
+ unregister();
104
111
  resolve({ ok: false, exitCode: -1, error: 'install hung — pty onExit never fired' });
105
112
  }, KILL_AFTER_MS + KILL_GRACE_MS + 30_000);
106
113
  if (deadman.unref) deadman.unref();
@@ -113,7 +120,6 @@ function install({ slug }) {
113
120
  lineBuf = lineBuf.slice(nl + 1);
114
121
  send('plugins:install-progress', { slug, line });
115
122
  }
116
- // Guard runaway buffers without newlines.
117
123
  if (lineBuf.length > MAX_LINE_BYTES) {
118
124
  send('plugins:install-progress', { slug, line: lineBuf });
119
125
  lineBuf = '';
@@ -129,13 +135,81 @@ function install({ slug }) {
129
135
  send('plugins:install-progress', { slug, line: lineBuf });
130
136
  lineBuf = '';
131
137
  }
132
- inFlight.delete(slug);
138
+ unregister();
133
139
  const code = typeof exitCode === 'number' ? exitCode : -1;
134
140
  resolve({ ok: code === 0, exitCode: code });
135
141
  });
136
142
  });
137
143
  }
138
144
 
145
+ /**
146
+ * Install a plugin. Without `marketplace`, runs `claude plugin install <slug>`
147
+ * (official catalog is pre-registered). With `marketplace: { add, name }`,
148
+ * first runs `claude plugin marketplace add <add>` to register the source, then
149
+ * installs `<slug>@<name>` — this is what a non-official plugin (e.g. the
150
+ * bundled session-manager-dev) needs on a fresh machine.
151
+ */
152
+ async function install({ slug, marketplace }) {
153
+ if (typeof slug !== 'string' || !SLUG_RE.test(slug) || slug.length > 128) {
154
+ return { ok: false, exitCode: -1, error: 'invalid slug' };
155
+ }
156
+ let mkt = null;
157
+ if (marketplace != null) {
158
+ const { add, name } = marketplace;
159
+ const addValid = typeof add === 'string' && add.length <= 200 &&
160
+ (add === BUNDLED_ADD || MKT_ADD_RE.test(add));
161
+ if (!addValid || typeof name !== 'string' || !MKT_NAME_RE.test(name) || name.length > 128) {
162
+ return { ok: false, exitCode: -1, error: 'invalid marketplace' };
163
+ }
164
+ // Resolve the `bundled` sentinel to the app's own packaged marketplace dir.
165
+ let addArg = add;
166
+ if (add === BUNDLED_ADD) {
167
+ const dir = bundledMarketplaceDir();
168
+ if (!fs.existsSync(path.join(dir, '.claude-plugin', 'marketplace.json'))) {
169
+ return { ok: false, exitCode: -1, error: 'bundled marketplace not found' };
170
+ }
171
+ addArg = dir;
172
+ }
173
+ mkt = { add: addArg, name };
174
+ }
175
+ if (inFlight.has(slug)) {
176
+ return { ok: false, exitCode: -1, error: 'install already in progress' };
177
+ }
178
+
179
+ // Reserve the slug for the whole sequence; each step swaps in its live proc.
180
+ inFlight.set(slug, null);
181
+ const register = (proc) => inFlight.set(slug, proc);
182
+ const release = () => inFlight.delete(slug);
183
+ // Between steps the slug stays reserved (set to null) so a concurrent call
184
+ // can't slip in; runStep's unregister only clears the per-step proc handle.
185
+ const unregisterStep = () => inFlight.set(slug, null);
186
+
187
+ try {
188
+ if (mkt) {
189
+ const added = await runStep({
190
+ slug,
191
+ args: ['plugin', 'marketplace', 'add', mkt.add],
192
+ register,
193
+ unregister: unregisterStep,
194
+ });
195
+ // Tolerate "already added" — the add command exits non-zero when the
196
+ // marketplace is already registered; fall through to install regardless.
197
+ if (!added.ok) {
198
+ send('plugins:install-progress', { slug, line: `[marketplace add exit ${added.exitCode} — continuing to install]` });
199
+ }
200
+ }
201
+ const installTarget = mkt ? `${slug}@${mkt.name}` : slug;
202
+ return await runStep({
203
+ slug,
204
+ args: ['plugin', 'install', installTarget],
205
+ register,
206
+ unregister: unregisterStep,
207
+ });
208
+ } finally {
209
+ release();
210
+ }
211
+ }
212
+
139
213
  function registerPluginInstallHandlers() {
140
214
  ipcMain.handle('plugins:install', async (_e, payload) => {
141
215
  // safeParse to preserve the existing `{ ok:false, exitCode:-1, error }`
@@ -145,7 +219,7 @@ function registerPluginInstallHandlers() {
145
219
  if (!parsed.success) {
146
220
  return { ok: false, exitCode: -1, error: 'invalid slug' };
147
221
  }
148
- return install({ slug: parsed.data.slug });
222
+ return install({ slug: parsed.data.slug, marketplace: parsed.data.marketplace });
149
223
  });
150
224
 
151
225
  // plugins:abort — send SIGKILL to a stuck install and release the inFlight
@@ -164,4 +238,4 @@ function registerPluginInstallHandlers() {
164
238
  });
165
239
  }
166
240
 
167
- module.exports = { registerPluginInstallHandlers, attachWindow };
241
+ module.exports = { registerPluginInstallHandlers, attachWindow, install, bundledMarketplaceDir };
package/src/main/pty.cjs CHANGED
@@ -4,7 +4,7 @@ const path = require('node:path');
4
4
  const os = require('node:os');
5
5
  const fs = require('node:fs');
6
6
  const { addAllowedRoot } = require('./config.cjs');
7
- const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
7
+ const { cleanChildEnv, pathWithUserBins } = require('./lib/cleanEnv.cjs');
8
8
  const { checkInsideHome } = require('./lib/insideHome.cjs');
9
9
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
10
10
 
@@ -62,18 +62,11 @@ class PtyManager {
62
62
  return { pid: existing.proc.pid, cwd: existing.cwd, reattached: true };
63
63
  }
64
64
 
65
- // Prefer `claude` on PATH; extend PATH with common user bin dirs since
66
- // Electron can launch with a stripped environment.
67
- const extraPath = [
68
- path.join(os.homedir(), '.local', 'bin'),
69
- path.join(os.homedir(), '.npm-global', 'bin'),
70
- '/usr/local/bin',
71
- '/usr/bin',
72
- '/bin',
73
- ].join(':');
74
-
65
+ // Prefer `claude` on PATH; extend PATH with common user + Homebrew bin dirs
66
+ // since Electron can launch with a stripped environment (Apple Silicon
67
+ // Homebrew is /opt/homebrew, absent from the default Electron PATH).
75
68
  const env = cleanChildEnv({
76
- PATH: `${extraPath}:${process.env.PATH || ''}`,
69
+ PATH: pathWithUserBins(),
77
70
  TERM: 'xterm-256color',
78
71
  COLORTERM: 'truecolor',
79
72
  FORCE_COLOR: '1',
@@ -48,7 +48,7 @@ const { randomUUID } = require('node:crypto');
48
48
  const { execFile } = require('node:child_process');
49
49
  const { ipcMain } = require('electron');
50
50
  const billing = require('./usage.cjs');
51
- const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
51
+ const { cleanChildEnv, pathWithUserBins } = require('./lib/cleanEnv.cjs');
52
52
  const supervisor = require('./supervisor.cjs');
53
53
  const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
54
54
  const { readTail } = require('./lib/fileTail.cjs');
@@ -921,7 +921,9 @@ async function executeJob(job, runDir, defaultCwd, onPid) {
921
921
  // Strip Claude Code env and secrets that leak in when session-manager is
922
922
  // launched from a `claude` shell. CLAUDE_EFFORT=xhigh forces Opus and
923
923
  // overrides `--model sonnet`, so scheduled jobs burn Opus credits silently.
924
- const childEnv = cleanChildEnv();
924
+ // PATH must include Homebrew/user bins or the job's node/git children ENOENT
925
+ // when Electron was launched from Finder/Dock on macOS (stripped PATH).
926
+ const childEnv = cleanChildEnv({ PATH: pathWithUserBins() });
925
927
 
926
928
  // Track whether the agent has emitted a `result` event in its JSONL stream.
927
929
  // null until seen; then one of "success" | "error_max_turns" | … per the
@@ -1230,7 +1232,7 @@ DO NOT attempt the fix. ONLY write the file. When the file exists, exit immediat
1230
1232
  }
1231
1233
 
1232
1234
  const claudeBin = resolveClaudeBin();
1233
- const childEnv = cleanChildEnv();
1235
+ const childEnv = cleanChildEnv({ PATH: pathWithUserBins() }); // Homebrew/user bins for macOS
1234
1236
 
1235
1237
  // Investigation needs only a deadman watchdog — no idle-tail or result-tail
1236
1238
  // since investigations are short-running Opus probes with a hard ceiling.
@@ -1685,6 +1687,34 @@ function pollRecoveryClearSource(pauseReason, hasCachedReset) {
1685
1687
  async function pollLoop() {
1686
1688
  try {
1687
1689
  await reapDeadRunningJobs().catch(() => {});
1690
+
1691
+ // Enterprise auth (Bedrock / Vertex / API-key / corporate gateway): there is
1692
+ // no consumer 5-hour usage meter to poll. Don't hit an endpoint that will
1693
+ // 404/time-out and eventually pause the queue on 'network' — treat usage as
1694
+ // wide-open and fire on pending + memory alone. (Blackrock-style machines.)
1695
+ if (!billing.usageMeterApplicable()) {
1696
+ cachedUtilization = 0;
1697
+ consecutiveFailures = 0;
1698
+ backoffMs = 0;
1699
+ backoffNextAt = null;
1700
+ firstFailureAt = null;
1701
+ firstNon429FailureAt = null;
1702
+ lastFailureKind = null;
1703
+ lastPollAt = Date.now();
1704
+ lastPollOk = true;
1705
+ persistSchedulerState();
1706
+ let cur = await readQueue();
1707
+ // Clear a stale auth/network pause inherited from a prior consumer-auth
1708
+ // session so the queue isn't wedged. Never clears rate_limit/manual.
1709
+ if (cur.paused && (cur.paused.reason === 'auth' || cur.paused.reason === 'network')) {
1710
+ await clearPause('enterprise-auth');
1711
+ cur = await readQueue(); // re-read so the cleared pause is visible to launch THIS cycle
1712
+ }
1713
+ await maybeLaunchWhenAvailable(cur);
1714
+ await broadcast();
1715
+ return; // finally re-arms the timer
1716
+ }
1717
+
1688
1718
  const r = await billing.fetchUsage();
1689
1719
 
1690
1720
  if (r.kind === 'ok') {
@@ -1701,9 +1731,12 @@ async function pollLoop() {
1701
1731
  persistSchedulerState();
1702
1732
 
1703
1733
  // Clear any pause that was waiting for a successful billing read.
1704
- const cur = await readQueue();
1734
+ let cur = await readQueue();
1705
1735
  const clearSrc = pollRecoveryClearSource(cur.paused?.reason ?? null, !!cachedNextReset);
1706
- if (clearSrc) await clearPause(clearSrc);
1736
+ if (clearSrc) {
1737
+ await clearPause(clearSrc);
1738
+ cur = await readQueue(); // re-read so the cleared pause launches work THIS cycle
1739
+ }
1707
1740
 
1708
1741
  await maybeLaunchWhenAvailable(cur);
1709
1742
  await broadcast();
@@ -0,0 +1,95 @@
1
+ /**
2
+ * First-boot seeder for the bundled `session-manager-dev` plugin.
3
+ *
4
+ * The plugin (its 10 dev skills) ships inside the npx distribution. To make it
5
+ * a true default, the app installs it on first launch from its own bundled
6
+ * marketplace (offline — no GitHub/registry). Idempotent two ways:
7
+ *
8
+ * 1. A marker file (`~/.claude/session-manager/.dev-plugin-seeded`) records
9
+ * seed state. Once the install SUCCEEDS we write `done` and never touch it
10
+ * again, so a deliberate uninstall stays uninstalled. A FAILED attempt only
11
+ * bumps an attempt counter and retries on the next few boots — a transient
12
+ * first-boot failure (e.g. `claude` not yet on PATH, briefly offline) must
13
+ * not permanently skip the default plugin. After MAX_ATTEMPTS we give up
14
+ * (manual install via the Plugins library button is always available).
15
+ * 2. Before attempting, we check settings.json `enabledPlugins` — if the
16
+ * plugin is already enabled (e.g. installed by hand), we just write the
17
+ * marker and skip the install.
18
+ *
19
+ * Fire-and-forget: called post-window from index.cjs. Errors are logged, never
20
+ * thrown. Kill-switch: SM_SEED_DEV_PLUGIN_DISABLE=1.
21
+ */
22
+
23
+ const fs = require('node:fs');
24
+ const path = require('node:path');
25
+ const os = require('node:os');
26
+ const { install } = require('./pluginInstall.cjs');
27
+
28
+ const PLUGIN_SLUG = 'session-manager-dev';
29
+ const MARKETPLACE = { add: 'bundled', name: 'session-manager' };
30
+ const ENABLED_KEY = `${PLUGIN_SLUG}@${MARKETPLACE.name}`;
31
+ const MAX_ATTEMPTS = 3; // give a transient first-boot failure a few chances
32
+
33
+ function markerPath() {
34
+ return path.join(os.homedir(), '.claude', 'session-manager', '.dev-plugin-seeded');
35
+ }
36
+
37
+ /** Read the marker → { done:boolean, attempts:number }. Absent = fresh. */
38
+ function readMarker() {
39
+ try {
40
+ const raw = fs.readFileSync(markerPath(), 'utf8').trim();
41
+ const m = JSON.parse(raw);
42
+ return { done: !!m.done, attempts: Number(m.attempts) || 0 };
43
+ } catch {
44
+ return { done: false, attempts: 0 };
45
+ }
46
+ }
47
+
48
+ function alreadyEnabled() {
49
+ try {
50
+ const raw = fs.readFileSync(path.join(os.homedir(), '.claude', 'settings.json'), 'utf8');
51
+ const enabled = JSON.parse(raw)?.enabledPlugins;
52
+ return !!(enabled && enabled[ENABLED_KEY]);
53
+ } catch {
54
+ return false; // settings absent/unparseable — treat as not-enabled.
55
+ }
56
+ }
57
+
58
+ function writeMarker(state) {
59
+ try {
60
+ const p = markerPath();
61
+ fs.mkdirSync(path.dirname(p), { recursive: true });
62
+ fs.writeFileSync(p, JSON.stringify({ ...state, ts: new Date().toISOString() }) + '\n');
63
+ } catch (err) {
64
+ console.warn('[seedDevPlugin] could not write marker:', err?.message ?? err);
65
+ }
66
+ }
67
+
68
+ async function seedDevPlugin({ logger = console } = {}) {
69
+ if (process.env.SM_SEED_DEV_PLUGIN_DISABLE === '1') return;
70
+ const marker = readMarker();
71
+ if (marker.done) return; // succeeded before — leave it alone.
72
+ if (marker.attempts >= MAX_ATTEMPTS) return; // gave up — manual install only.
73
+
74
+ try {
75
+ if (alreadyEnabled()) {
76
+ writeMarker({ done: true, attempts: marker.attempts });
77
+ return;
78
+ }
79
+ logger.log?.('[seedDevPlugin] installing bundled session-manager-dev plugin…');
80
+ const r = await install({ slug: PLUGIN_SLUG, marketplace: MARKETPLACE });
81
+ if (r.ok) {
82
+ logger.log?.('[seedDevPlugin] installed session-manager-dev@session-manager');
83
+ writeMarker({ done: true, attempts: marker.attempts });
84
+ } else {
85
+ // Failure: bump the attempt counter so the next boot can retry (bounded).
86
+ logger.warn?.(`[seedDevPlugin] install failed (exit ${r.exitCode})${r.error ? ` — ${r.error}` : ''}; attempt ${marker.attempts + 1}/${MAX_ATTEMPTS}`);
87
+ writeMarker({ done: false, attempts: marker.attempts + 1 });
88
+ }
89
+ } catch (err) {
90
+ logger.warn?.('[seedDevPlugin] error:', err?.message ?? err);
91
+ writeMarker({ done: false, attempts: marker.attempts + 1 });
92
+ }
93
+ }
94
+
95
+ module.exports = { seedDevPlugin };
@@ -24,6 +24,39 @@ const { refreshIfNeeded, expiresAtMs } = require('./lib/credentials.cjs');
24
24
  const { writeJson } = require('./config.cjs');
25
25
 
26
26
  const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
27
+
28
+ /** A non-empty env value that isn't an explicit falsey string. */
29
+ function envEnabled(v) {
30
+ return v != null && v !== '' && v !== '0' && String(v).toLowerCase() !== 'false';
31
+ }
32
+
33
+ /**
34
+ * Is the consumer 5-hour usage meter (/api/oauth/usage) even applicable here?
35
+ *
36
+ * That endpoint only exists for OAuth/subscription auth against
37
+ * api.anthropic.com. Enterprise auth modes have no such meter, so polling it
38
+ * just 404s/times-out — and the scheduler must NOT gate on (or pause for) it.
39
+ * Detected modes: Amazon Bedrock, Google Vertex, raw API-key, a custom auth
40
+ * token, or a non-Anthropic base URL (corporate gateway/proxy).
41
+ *
42
+ * Returns false → caller should treat usage as unavailable-by-design and fire
43
+ * work on its own (pending + memory) instead of waiting on a meter.
44
+ */
45
+ function usageMeterApplicable(env = process.env) {
46
+ if (envEnabled(env.CLAUDE_CODE_USE_BEDROCK)) return false;
47
+ if (envEnabled(env.CLAUDE_CODE_USE_VERTEX)) return false;
48
+ if (env.ANTHROPIC_API_KEY) return false;
49
+ if (env.ANTHROPIC_AUTH_TOKEN) return false;
50
+ if (env.ANTHROPIC_BASE_URL) {
51
+ // Parse the host rather than substring-match, so a deceptive gateway like
52
+ // https://anthropic.com.attacker.example is correctly treated as enterprise.
53
+ let host;
54
+ try { host = new URL(env.ANTHROPIC_BASE_URL).hostname.toLowerCase(); }
55
+ catch { return false; } // unparseable custom URL → treat as a gateway
56
+ if (host !== 'anthropic.com' && !host.endsWith('.anthropic.com')) return false;
57
+ }
58
+ return true;
59
+ }
27
60
  const CACHE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'billing-cache.json');
28
61
  // Coalesce the 4 renderer pollers (Overview/AppStatusBar/StatusBar/Usage). A
29
62
  // fresh ok-cache is served directly without touching the network. Auth/
@@ -164,4 +197,4 @@ function registerBillingHandlers() {
164
197
  });
165
198
  }
166
199
 
167
- module.exports = { registerBillingHandlers, fetchUsage, classifyUsageResponse };
200
+ module.exports = { registerBillingHandlers, fetchUsage, classifyUsageResponse, usageMeterApplicable };
@@ -1025,8 +1025,14 @@ export interface SessionManagerAPI {
1025
1025
  };
1026
1026
  plugins: {
1027
1027
  /** Run `claude plugin install <slug>` in a hidden pty. Streams output
1028
- * via `onInstallProgress`. Returns { ok, exitCode } on exit. */
1029
- install: (payload: { slug: string }) => Promise<PluginInstallResult>;
1028
+ * via `onInstallProgress`. Returns { ok, exitCode } on exit.
1029
+ * Pass `marketplace` for a non-official plugin: the source is registered
1030
+ * via `claude plugin marketplace add <add>` first, then `<slug>@<name>`
1031
+ * is installed. */
1032
+ install: (payload: {
1033
+ slug: string;
1034
+ marketplace?: { add: string; name: string };
1035
+ }) => Promise<PluginInstallResult>;
1030
1036
  onInstallProgress: (handler: (ev: PluginInstallProgressEvent) => void) => () => void;
1031
1037
  };
1032
1038
  clipboard: {