@moderniser/plugin 0.1.9 → 0.1.10

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/client.d.ts CHANGED
@@ -3,6 +3,13 @@ export declare function setStoredKey(key: string): void;
3
3
  export declare function storedKey(): string | undefined;
4
4
  export declare function resolveKey(): string | undefined;
5
5
  export declare const COMPANY_HEADER = "x-moderniser-company";
6
+ export declare function writeStoredCompany(companyId: string): {
7
+ ok: true;
8
+ } | {
9
+ ok: false;
10
+ error: string;
11
+ };
6
12
  export declare function setStoredCompany(companyId: string): void;
7
13
  export declare function resolveCompany(): string | undefined;
8
14
  export declare function companyHeaders(): Record<string, string>;
15
+ export declare const SESSION_HEADER = "x-moderniser-session";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
3
 
4
4
  // packages/plugin/src/client.ts
5
- import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
5
+ import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  var API = () => (process.env.MODERNISER_API_URL ?? "https://v2.moderniser.ai/api").replace(/\/+$/, "");
@@ -36,10 +36,7 @@ function companyFilePath() {
36
36
  const data = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share");
37
37
  return join(data, "moderniser", "company.json");
38
38
  }
39
- var storedCompany;
40
- function setStoredCompany(companyId) {
41
- storedCompany = companyId;
42
- fileCompany = companyId;
39
+ function writeStoredCompany(companyId) {
43
40
  try {
44
41
  const path = companyFilePath();
45
42
  mkdirSync(join(path, ".."), { recursive: true });
@@ -47,28 +44,45 @@ function setStoredCompany(companyId) {
47
44
  writeFileSync(temp, JSON.stringify({ companyId }, null, 2) + `
48
45
  `, { mode: 384 });
49
46
  renameSync(temp, path);
50
- } catch {}
47
+ fileCompanyMtime = undefined;
48
+ return { ok: true };
49
+ } catch (error) {
50
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
51
+ }
51
52
  }
53
+ function setStoredCompany(companyId) {
54
+ const result = writeStoredCompany(companyId);
55
+ if (!result.ok)
56
+ return;
57
+ }
58
+ var fileCompanyMtime;
52
59
  var fileCompany;
53
60
  function companyFromFile() {
54
- if (fileCompany !== undefined)
55
- return fileCompany ?? undefined;
56
- fileCompany = null;
61
+ let mtime;
62
+ try {
63
+ mtime = statSync(companyFilePath()).mtimeMs;
64
+ } catch {
65
+ return;
66
+ }
67
+ if (fileCompanyMtime === mtime)
68
+ return fileCompany;
57
69
  try {
58
70
  const parsed = JSON.parse(readFileSync(companyFilePath(), "utf8"));
59
- if (typeof parsed?.companyId === "string" && parsed.companyId.length > 0) {
60
- fileCompany = parsed.companyId;
61
- }
62
- } catch {}
63
- return fileCompany ?? undefined;
71
+ fileCompany = typeof parsed?.companyId === "string" && parsed.companyId.length > 0 ? parsed.companyId : undefined;
72
+ fileCompanyMtime = mtime;
73
+ return fileCompany;
74
+ } catch {
75
+ return fileCompany;
76
+ }
64
77
  }
65
78
  function resolveCompany() {
66
- return process.env.MODERNISER_COMPANY ?? storedCompany ?? companyFromFile();
79
+ return process.env.MODERNISER_COMPANY ?? companyFromFile();
67
80
  }
68
81
  function companyHeaders() {
69
82
  const company = resolveCompany();
70
83
  return company ? { [COMPANY_HEADER]: company } : {};
71
84
  }
85
+ var SESSION_HEADER = "x-moderniser-session";
72
86
 
73
87
  // packages/plugin/src/auth.ts
74
88
  async function start() {
@@ -161,7 +175,13 @@ function moderniserContext() {
161
175
  "- Task: one unit of work a team would plan together.",
162
176
  "- Subtask: one concrete change in one codebase, small enough to finish and check off.",
163
177
  "",
164
- "When the user says project, task, board or ticket, they mean Moderniser's — " + "reach for the board tools (moderniser_list_projects first, then " + "moderniser_create_task / moderniser_update_task / moderniser_create_subtask / …). " + "Do not answer by scaffolding files or writing a spec document instead, " + "unless a document is what they actually asked for."
178
+ "When the user says project, task, board or ticket, they mean Moderniser's.",
179
+ "",
180
+ "When a request is genuinely ambiguous — it could be the work itself, a " + "write-up about it, or a board item, and nothing in it says which — ask " + "which route they want before acting, in one short line with three " + "options: Solve it (implement/fix it directly in the codebase now), " + "Analyse it (investigate and report findings, no code changes, no board " + "writes), Track it (create the project/task/subtasks on the board — " + "moderniser_list_projects first, then moderniser_create_task / " + "moderniser_create_subtask, never a scaffolded file or a spec document " + "written in their place).",
181
+ "",
182
+ "A direct instruction to change code is not ambiguous — do it. Ask once; " + "once answered, proceed without re-asking for the rest of the thread. If " + 'the user names the route explicitly ("create a task for…", ' + '"just fix it", "investigate why…"), skip the question and do that.',
183
+ "",
184
+ "Board writes (moderniser_create_task, moderniser_create_subtask, " + "moderniser_update_task, …) happen only when tracking is what was asked " + "for or chosen — never as a side effect of an implementation or analysis " + "request. Read-only calls (moderniser_list_projects, reads, Context Hub " + "search) stay free to use for context at any time."
165
185
  ];
166
186
  const workspace = process.env.MODERNISER_WORKSPACE_NAME;
167
187
  const company = process.env.MODERNISER_COMPANY_NAME;
@@ -188,17 +208,30 @@ var GATEWAY = () => {
188
208
  };
189
209
  var PROVIDER_ID = "moderniser";
190
210
  var DEFAULT_MODEL = `${PROVIDER_ID}/emm`;
191
- async function call(path, body) {
211
+ async function call(path, body, extraHeaders) {
192
212
  const key = resolveKey();
193
213
  if (!key)
194
214
  return null;
195
215
  try {
196
- return await fetch(`${API2()}${path}`, {
216
+ const res = await fetch(`${API2()}${path}`, {
197
217
  method: "POST",
198
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}`, ...companyHeaders() },
218
+ headers: {
219
+ "Content-Type": "application/json",
220
+ Authorization: `Bearer ${key}`,
221
+ ...companyHeaders(),
222
+ ...extraHeaders
223
+ },
199
224
  body: JSON.stringify(body),
200
225
  signal: AbortSignal.timeout(1e4)
201
226
  });
227
+ if (res.ok) {
228
+ try {
229
+ return { ok: true, body: await res.json() };
230
+ } catch {
231
+ return { ok: true, body: {} };
232
+ }
233
+ }
234
+ return null;
202
235
  } catch {
203
236
  return null;
204
237
  }
@@ -258,8 +291,68 @@ async function gatewayProduct() {
258
291
  }
259
292
  }
260
293
  var registered = new Set;
294
+ var sessionCompanies = new Map;
295
+ var unbound = new Set;
296
+ async function boundCompany(sessionId) {
297
+ const key = resolveKey();
298
+ if (!key)
299
+ return;
300
+ if (unbound.has(sessionId))
301
+ return;
302
+ try {
303
+ const res = await fetch(`${API2()}/billing/sessions/${encodeURIComponent(sessionId)}`, {
304
+ headers: { Authorization: `Bearer ${key}`, ...companyHeaders() },
305
+ signal: AbortSignal.timeout(5000)
306
+ });
307
+ if (!res.ok) {
308
+ if (res.status === 404)
309
+ unbound.add(sessionId);
310
+ return;
311
+ }
312
+ const body = await res.json();
313
+ if (typeof body.companyId !== "string" || body.companyId.length === 0)
314
+ return;
315
+ const found = {
316
+ companyId: body.companyId,
317
+ ...typeof body.companyName === "string" && body.companyName ? { companyName: body.companyName } : {}
318
+ };
319
+ sessionCompanies.set(sessionId, found);
320
+ return found;
321
+ } catch {
322
+ return;
323
+ }
324
+ }
325
+ async function sessionCompany(sessionId, parent) {
326
+ if (sessionId) {
327
+ const own = sessionCompanies.get(sessionId) ?? await boundCompany(sessionId);
328
+ if (own)
329
+ return { [COMPANY_HEADER]: own.companyId };
330
+ const parentId = await parent?.();
331
+ if (parentId && parentId !== sessionId) {
332
+ const bound = sessionCompanies.get(parentId) ?? await boundCompany(parentId);
333
+ if (bound) {
334
+ sessionCompanies.set(sessionId, bound);
335
+ return { [COMPANY_HEADER]: bound.companyId };
336
+ }
337
+ }
338
+ }
339
+ return companyHeaders();
340
+ }
341
+ var client;
342
+ async function parentOf(sessionId) {
343
+ if (!client)
344
+ return;
345
+ try {
346
+ const res = await client.session.get({ path: { id: sessionId } });
347
+ const parentID = res.data?.parentID;
348
+ return typeof parentID === "string" && parentID ? parentID : undefined;
349
+ } catch {
350
+ return;
351
+ }
352
+ }
261
353
  var reported = new Map;
262
- var ModerniserPlugin = async () => {
354
+ var ModerniserPlugin = async (input) => {
355
+ client = input.client;
263
356
  return {
264
357
  config: async (config) => {
265
358
  const modalities = { input: ["text", "image"], output: ["text"] };
@@ -278,15 +371,13 @@ var ModerniserPlugin = async () => {
278
371
  }
279
372
  };
280
373
  const key = KEY();
281
- const company = companyHeaders();
282
374
  config.provider ??= {};
283
375
  config.provider[PROVIDER_ID] ??= {
284
376
  name: "Moderniser",
285
377
  npm: "@ai-sdk/openai-compatible",
286
378
  options: {
287
379
  baseURL: GATEWAY(),
288
- ...key ? { apiKey: key } : {},
289
- ...Object.keys(company).length ? { headers: company } : {}
380
+ ...key ? { apiKey: key } : {}
290
381
  },
291
382
  models
292
383
  };
@@ -310,14 +401,38 @@ var ModerniserPlugin = async () => {
310
401
  "experimental.chat.system.transform": async (_input, output) => {
311
402
  output.system.push(moderniserContext());
312
403
  },
404
+ "chat.headers": async (input2, output) => {
405
+ output.headers[SESSION_HEADER] = input2.sessionID;
406
+ const company = await sessionCompany(input2.sessionID, () => parentOf(input2.sessionID));
407
+ for (const [name, value] of Object.entries(company))
408
+ output.headers[name] = value;
409
+ },
313
410
  auth: moderniserAuth(),
314
411
  event: async ({ event }) => {
315
412
  const type = event?.type;
316
- if (type === "session.updated" || type === "session.status") {
413
+ if (type === "session.created" || type === "session.updated" || type === "session.status") {
317
414
  const sessionId = event.properties?.info?.id ?? event.properties?.sessionID;
318
415
  if (typeof sessionId === "string" && !registered.has(sessionId)) {
319
416
  registered.add(sessionId);
320
- await call("/billing/sessions", { sessionId });
417
+ const info2 = event.properties?.info;
418
+ const fromEvent = info2?.parentID;
419
+ const parentId = info2 ? typeof fromEvent === "string" && fromEvent ? fromEvent : undefined : await parentOf(sessionId);
420
+ const bindTo = parentId ? await sessionCompany(parentId) : companyHeaders();
421
+ if (parentId) {
422
+ const inherited = sessionCompanies.get(parentId);
423
+ if (inherited)
424
+ sessionCompanies.set(sessionId, inherited);
425
+ }
426
+ const res = await call("/billing/sessions", { sessionId }, { ...bindTo, [SESSION_HEADER]: sessionId });
427
+ if (res?.ok) {
428
+ const body = res.body;
429
+ if (typeof body.companyId === "string" && body.companyId) {
430
+ sessionCompanies.set(sessionId, {
431
+ companyId: body.companyId,
432
+ ...typeof body.companyName === "string" && body.companyName ? { companyName: body.companyName } : {}
433
+ });
434
+ }
435
+ }
321
436
  }
322
437
  return;
323
438
  }
package/dist/tui.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { TuiPlugin } from "@opencode-ai/plugin/tui";
2
+ declare const plugin: {
3
+ id: string;
4
+ tui: TuiPlugin;
5
+ };
6
+ export default plugin;
package/dist/tui.js ADDED
@@ -0,0 +1,360 @@
1
+ // packages/plugin/src/tui.ts
2
+ import { createComponent, createMemo, createSignal, batch } from "solid-js";
3
+ import { createElement, spread } from "@opentui/solid";
4
+
5
+ // packages/plugin/src/client.ts
6
+ import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ var API = () => (process.env.MODERNISER_API_URL ?? "https://v2.moderniser.ai/api").replace(/\/+$/, "");
10
+ var stored;
11
+ function authFilePath() {
12
+ const data = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share");
13
+ return join(data, "moderniser", "auth.json");
14
+ }
15
+ var fileKey;
16
+ function fromAuthFile() {
17
+ if (fileKey !== undefined)
18
+ return fileKey ?? undefined;
19
+ fileKey = null;
20
+ try {
21
+ const parsed = JSON.parse(readFileSync(authFilePath(), "utf8"));
22
+ const entry = parsed?.moderniser;
23
+ if (typeof entry === "object" && entry !== null && entry.type === "api" && typeof entry.key === "string" && entry.key.length > 0) {
24
+ fileKey = entry.key;
25
+ }
26
+ } catch {}
27
+ return fileKey ?? undefined;
28
+ }
29
+ function resolveKey() {
30
+ return process.env.MODERNISER_API_KEY ?? stored ?? fromAuthFile();
31
+ }
32
+ var COMPANY_HEADER = "x-moderniser-company";
33
+ function companyFilePath() {
34
+ const data = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share");
35
+ return join(data, "moderniser", "company.json");
36
+ }
37
+ function writeStoredCompany(companyId) {
38
+ try {
39
+ const path = companyFilePath();
40
+ mkdirSync(join(path, ".."), { recursive: true });
41
+ const temp = `${path}.${process.pid}.tmp`;
42
+ writeFileSync(temp, JSON.stringify({ companyId }, null, 2) + `
43
+ `, { mode: 384 });
44
+ renameSync(temp, path);
45
+ fileCompanyMtime = undefined;
46
+ return { ok: true };
47
+ } catch (error) {
48
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
49
+ }
50
+ }
51
+ var fileCompanyMtime;
52
+ var fileCompany;
53
+ function companyFromFile() {
54
+ let mtime;
55
+ try {
56
+ mtime = statSync(companyFilePath()).mtimeMs;
57
+ } catch {
58
+ return;
59
+ }
60
+ if (fileCompanyMtime === mtime)
61
+ return fileCompany;
62
+ try {
63
+ const parsed = JSON.parse(readFileSync(companyFilePath(), "utf8"));
64
+ fileCompany = typeof parsed?.companyId === "string" && parsed.companyId.length > 0 ? parsed.companyId : undefined;
65
+ fileCompanyMtime = mtime;
66
+ return fileCompany;
67
+ } catch {
68
+ return fileCompany;
69
+ }
70
+ }
71
+ function resolveCompany() {
72
+ return process.env.MODERNISER_COMPANY ?? companyFromFile();
73
+ }
74
+
75
+ // packages/plugin/src/tui.ts
76
+ var box = (props) => {
77
+ const element = createElement("box");
78
+ spread(element, props);
79
+ return element;
80
+ };
81
+ var text = (props) => {
82
+ const element = createElement("text");
83
+ spread(element, props);
84
+ return element;
85
+ };
86
+ var tui = async (api) => {
87
+ let mcpCompany = resolveCompany();
88
+ const toast = (message, variant = "info", title) => api.ui.toast({ message, variant, title });
89
+ const cloudHeaders = (company) => ({
90
+ Authorization: `Bearer ${resolveKey()}`,
91
+ ...company ? { [COMPANY_HEADER]: company } : {}
92
+ });
93
+ async function currentCompany() {
94
+ const key = resolveKey();
95
+ if (!key)
96
+ return;
97
+ try {
98
+ const res = await fetch(`${API()}/auth/me`, { headers: cloudHeaders(), signal: AbortSignal.timeout(5000) });
99
+ if (!res.ok)
100
+ return;
101
+ const body = await res.json();
102
+ return typeof body.company?.id === "string" ? body.company.id : undefined;
103
+ } catch {
104
+ return;
105
+ }
106
+ }
107
+ async function listMemberships() {
108
+ const key = resolveKey();
109
+ if (!key)
110
+ return { error: "not signed in" };
111
+ try {
112
+ const res = await fetch(`${API()}/auth/me/companies`, {
113
+ headers: cloudHeaders(),
114
+ signal: AbortSignal.timeout(5000)
115
+ });
116
+ if (!res.ok)
117
+ return { error: `Moderniser answered ${res.status}` };
118
+ return await res.json();
119
+ } catch (error) {
120
+ return { error: error instanceof Error ? error.message : String(error) };
121
+ }
122
+ }
123
+ async function verify(target) {
124
+ const key = resolveKey();
125
+ if (!key)
126
+ return { ok: false, reason: "not signed in" };
127
+ try {
128
+ const res = await fetch(`${API()}/auth/me`, {
129
+ headers: cloudHeaders(target),
130
+ signal: AbortSignal.timeout(5000)
131
+ });
132
+ if (res.status === 404)
133
+ return { ok: false, reason: "you are no longer a member of it" };
134
+ if (res.status === 403)
135
+ return { ok: false, reason: "it is suspended" };
136
+ if (!res.ok)
137
+ return { ok: false, reason: `Moderniser answered ${res.status}` };
138
+ return { ok: true };
139
+ } catch (error) {
140
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
141
+ }
142
+ }
143
+ async function boundCompany(sessionId) {
144
+ const key = resolveKey();
145
+ if (!key)
146
+ return;
147
+ try {
148
+ const res = await fetch(`${API()}/billing/sessions/${encodeURIComponent(sessionId)}`, {
149
+ headers: cloudHeaders(),
150
+ signal: AbortSignal.timeout(5000)
151
+ });
152
+ if (!res.ok)
153
+ return;
154
+ const body = await res.json();
155
+ if (typeof body.companyId !== "string")
156
+ return;
157
+ return { id: body.companyId, name: typeof body.companyName === "string" ? body.companyName : body.companyId };
158
+ } catch {
159
+ return;
160
+ }
161
+ }
162
+ async function reheaderMcp(target) {
163
+ const key = resolveKey();
164
+ if (!key)
165
+ return false;
166
+ try {
167
+ const res = await api.client.mcp.add({
168
+ name: "moderniser",
169
+ config: {
170
+ type: "remote",
171
+ url: `${API()}/mcp`,
172
+ headers: { Authorization: `Bearer ${key}`, [COMPANY_HEADER]: target },
173
+ oauth: false
174
+ }
175
+ });
176
+ const status = res.data?.moderniser?.status;
177
+ if (status === "failed")
178
+ return false;
179
+ mcpCompany = target;
180
+ return true;
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+ let following;
186
+ async function followSession(sessionId) {
187
+ if (following === sessionId)
188
+ return;
189
+ following = sessionId;
190
+ const bound2 = await boundCompany(sessionId);
191
+ if (!bound2) {
192
+ following = undefined;
193
+ return;
194
+ }
195
+ if (mcpCompany !== bound2.id)
196
+ await reheaderMcp(bound2.id);
197
+ }
198
+ api.event.on("session.status", (event) => {
199
+ followSession(event.properties.sessionID);
200
+ });
201
+ api.event.on("session.updated", (event) => {
202
+ followSession(event.properties.info.id);
203
+ });
204
+ function rowsFor(companies, current, readOnly = false) {
205
+ const rows = companies.map((c) => ({
206
+ title: c.isHome ? `${c.name} (home)` : c.name,
207
+ value: c.id,
208
+ description: c.domain,
209
+ category: c.role,
210
+ footer: c.id === current ? readOnly ? "current — pinned by MODERNISER_COMPANY" : "current" : undefined,
211
+ disabled: readOnly || c.id === current
212
+ }));
213
+ return rows;
214
+ }
215
+ async function showPicker() {
216
+ if (!resolveKey()) {
217
+ toast("Sign in with `moderniser providers login` first", "warning", "Not signed in");
218
+ return;
219
+ }
220
+ const pin = process.env.MODERNISER_COMPANY;
221
+ if (pin) {
222
+ const companies2 = (await listMemberships()).companies;
223
+ if (!companies2) {
224
+ toast(`This environment is pinned to ${pin} by MODERNISER_COMPANY — unset it to switch companies`, "info", "Pinned by the environment");
225
+ return;
226
+ }
227
+ const pinned = companies2.find((c) => c.id === pin);
228
+ api.ui.dialog.replace(() => createComponent(api.ui.DialogSelect, {
229
+ title: pinned ? `Pinned to ${pinned.name} by MODERNISER_COMPANY` : "Pinned by MODERNISER_COMPANY",
230
+ options: rowsFor(companies2, pin, true),
231
+ current: pin
232
+ }));
233
+ return;
234
+ }
235
+ const route = api.route.current;
236
+ if (route.name === "session") {
237
+ const sessionId = route.params?.sessionID;
238
+ if (typeof sessionId === "string" && api.state.session.status(sessionId)?.type === "busy") {
239
+ toast("Finish or interrupt the current turn first", "warning", "Session busy");
240
+ return;
241
+ }
242
+ }
243
+ const listing = await listMemberships();
244
+ if (!listing.companies) {
245
+ toast(`Could not reach Moderniser: ${listing.error ?? "unknown error"}`, "error", "Offline");
246
+ return;
247
+ }
248
+ const companies = listing.companies;
249
+ const current = await currentCompany();
250
+ if (companies.length === 0) {
251
+ toast("You have no company memberships — sessions bill your home company. Ask an admin to add you.", "warning", "No companies");
252
+ return;
253
+ }
254
+ if (current && companies.every((c) => c.id === current)) {
255
+ const only = companies.find((c) => c.id === current);
256
+ toast(`Already acting for ${only?.name ?? "this company"}`, "info");
257
+ return;
258
+ }
259
+ const rows = rowsFor(companies, current);
260
+ api.ui.dialog.replace(() => createComponent(api.ui.DialogSelect, {
261
+ title: "Switch company",
262
+ options: rows,
263
+ current,
264
+ onSelect: (option) => {
265
+ switchTo(option.value, companies);
266
+ }
267
+ }));
268
+ }
269
+ async function switchTo(target, companies) {
270
+ api.ui.dialog.clear();
271
+ const chosen = companies.find((c) => c.id === target);
272
+ const name = chosen?.name ?? target;
273
+ const current = await currentCompany();
274
+ if (current === target) {
275
+ toast(`Already acting for ${name}`, "info");
276
+ return;
277
+ }
278
+ const verdict = await verify(target);
279
+ if (!verdict.ok) {
280
+ toast(`Cannot switch to ${name} — ${verdict.reason}. Nothing was changed.`, "error", "Not switched");
281
+ return;
282
+ }
283
+ const written = writeStoredCompany(target);
284
+ if (!written.ok) {
285
+ toast(`Cannot switch to ${name} — ${written.error}. Nothing was changed.`, "error", "Not switched");
286
+ return;
287
+ }
288
+ const wasConnectedAs = mcpCompany;
289
+ const reheaded = await reheaderMcp(target);
290
+ if (!reheaded) {
291
+ const previous = wasConnectedAs ? companies.find((c) => c.id === wasConnectedAs)?.name ?? wasConnectedAs : "the previous company";
292
+ toast(`Switched to ${name} — but Moderniser tools are still connected as ${previous}. Restart to fix.`, "warning", "Partly switched");
293
+ } else {
294
+ toast(`Now acting for ${name}. The next prompt starts a new session.`, "success", "Switched");
295
+ }
296
+ api.route.navigate("home");
297
+ }
298
+ api.keymap.registerLayer({
299
+ commands: [
300
+ {
301
+ name: "moderniser.company",
302
+ title: "Switch company",
303
+ desc: "Choose which company pays for future sessions",
304
+ category: "Moderniser",
305
+ namespace: "palette",
306
+ slashName: "company",
307
+ run() {
308
+ showPicker();
309
+ }
310
+ }
311
+ ]
312
+ });
313
+ const [bound, setBound] = createSignal(undefined);
314
+ const [boundFor, setBoundFor] = createSignal(undefined);
315
+ const pending = new Set;
316
+ api.slots.register({
317
+ order: 90,
318
+ slots: {
319
+ sidebar_footer(_ctx, props) {
320
+ return createMemo(() => {
321
+ const sessionId = props.session_id;
322
+ const known = boundFor() === sessionId ? bound() : undefined;
323
+ if (!known && !pending.has(sessionId)) {
324
+ pending.add(sessionId);
325
+ boundCompany(sessionId).then((found) => {
326
+ pending.delete(sessionId);
327
+ batch(() => {
328
+ setBound(found);
329
+ setBoundFor(sessionId);
330
+ });
331
+ });
332
+ }
333
+ if (!known)
334
+ return null;
335
+ return box({
336
+ backgroundColor: api.theme.current.backgroundElement,
337
+ paddingTop: 1,
338
+ paddingBottom: 1,
339
+ paddingLeft: 2,
340
+ paddingRight: 2,
341
+ flexDirection: "row",
342
+ gap: 1,
343
+ children: [
344
+ text({ fg: api.theme.current.text, content: "Billed to" }),
345
+ text({ fg: api.theme.current.textMuted, content: known.name })
346
+ ]
347
+ });
348
+ })();
349
+ }
350
+ }
351
+ });
352
+ };
353
+ var plugin = {
354
+ id: "internal:moderniser-company",
355
+ tui
356
+ };
357
+ var tui_default = plugin;
358
+ export {
359
+ tui_default as default
360
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moderniser/plugin",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "description": "Moderniser plugin for opencode: points inference at the gateway, declares the Moderniser remote MCP server, and reports session telemetry",
6
6
  "license": "MIT",
@@ -11,6 +11,17 @@
11
11
  },
12
12
  "homepage": "https://v2.moderniser.ai",
13
13
  "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./tui": {
21
+ "types": "./dist/tui.d.ts",
22
+ "default": "./dist/tui.js"
23
+ }
24
+ },
14
25
  "files": [
15
26
  "dist"
16
27
  ],
@@ -20,8 +31,34 @@
20
31
  "engines": {
21
32
  "node": ">=20"
22
33
  },
34
+ "devDependencies": {
35
+ "@opentui/core": "0.4.5",
36
+ "@opentui/keymap": "0.4.5",
37
+ "@opentui/solid": "0.4.5",
38
+ "solid-js": "1.9.10"
39
+ },
23
40
  "peerDependencies": {
24
- "@opencode-ai/plugin": "*"
41
+ "@opencode-ai/plugin": "*",
42
+ "@opentui/core": ">=0.4.5",
43
+ "@opentui/keymap": ">=0.4.5",
44
+ "@opentui/solid": ">=0.4.5",
45
+ "solid-js": ">=1.9.0"
25
46
  },
26
- "types": "./dist/index.d.ts"
47
+ "peerDependenciesMeta": {
48
+ "@opencode-ai/plugin": {
49
+ "optional": false
50
+ },
51
+ "@opentui/core": {
52
+ "optional": true
53
+ },
54
+ "@opentui/keymap": {
55
+ "optional": true
56
+ },
57
+ "@opentui/solid": {
58
+ "optional": true
59
+ },
60
+ "solid-js": {
61
+ "optional": true
62
+ }
63
+ }
27
64
  }