@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 +7 -0
- package/dist/index.js +140 -25
- package/dist/tui.d.ts +6 -0
- package/dist/tui.js +360 -0
- package/package.json +40 -3
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
} catch {
|
|
63
|
-
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
216
|
+
const res = await fetch(`${API2()}${path}`, {
|
|
197
217
|
method: "POST",
|
|
198
|
-
headers: {
|
|
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
|
-
|
|
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
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.
|
|
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
|
-
"
|
|
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
|
}
|