@rimori/client 2.5.31 → 2.5.32-next.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.
@@ -11,6 +11,7 @@ export declare class Translator {
11
11
  private translationUrl;
12
12
  private ai;
13
13
  private aiTranslationCache;
14
+ private aiTranslationPending;
14
15
  constructor(initialLanguage: string, translationUrl: string, ai: AIModule);
15
16
  /**
16
17
  * Initialize translator with user's language
@@ -14,6 +14,7 @@ import { createInstance } from 'i18next';
14
14
  export class Translator {
15
15
  constructor(initialLanguage, translationUrl, ai) {
16
16
  this.aiTranslationCache = new Map();
17
+ this.aiTranslationPending = new Map();
17
18
  this.currentLanguage = initialLanguage;
18
19
  this.initializationState = 'not-inited';
19
20
  this.initializationPromise = null;
@@ -158,29 +159,38 @@ export class Translator {
158
159
  const cached = this.aiTranslationCache.get(text);
159
160
  if (cached)
160
161
  return cached;
161
- try {
162
- // If the current language is English, don't translate
163
- if (!this.ai || this.currentLanguage === 'en')
164
- return text;
165
- const response = yield this.ai.getObject({
166
- prompt: 'global.translator.translate',
167
- variables: {
168
- additionalInstructions: additionalInstructions !== null && additionalInstructions !== void 0 ? additionalInstructions : '',
169
- language: this.currentLanguage,
170
- text,
171
- },
172
- cache: true,
173
- });
174
- const translation = response === null || response === void 0 ? void 0 : response.translation;
175
- if (translation) {
176
- this.aiTranslationCache.set(text, translation);
177
- return translation;
162
+ const pending = this.aiTranslationPending.get(text);
163
+ if (pending)
164
+ return pending;
165
+ if (!this.ai || this.currentLanguage === 'en')
166
+ return text;
167
+ const promise = (() => __awaiter(this, void 0, void 0, function* () {
168
+ try {
169
+ const response = yield this.ai.getObject({
170
+ prompt: 'global.translator.translate',
171
+ variables: {
172
+ additionalInstructions: additionalInstructions !== null && additionalInstructions !== void 0 ? additionalInstructions : '',
173
+ language: this.currentLanguage,
174
+ text,
175
+ },
176
+ cache: true,
177
+ });
178
+ const translation = response === null || response === void 0 ? void 0 : response.translation;
179
+ if (translation) {
180
+ this.aiTranslationCache.set(text, translation);
181
+ return translation;
182
+ }
178
183
  }
179
- }
180
- catch (error) {
181
- console.warn('Failed to translate freeform text:', { text, error });
182
- }
183
- return text;
184
+ catch (error) {
185
+ console.warn('Failed to translate freeform text:', { text, error });
186
+ }
187
+ finally {
188
+ this.aiTranslationPending.delete(text);
189
+ }
190
+ return text;
191
+ }))();
192
+ this.aiTranslationPending.set(text, promise);
193
+ return promise;
184
194
  });
185
195
  }
186
196
  }
@@ -46,11 +46,11 @@ export interface RimoriInfo {
46
46
  ttsEnabled: boolean;
47
47
  }
48
48
  export declare class RimoriCommunicationHandler {
49
+ readonly pluginId: string;
49
50
  private port;
50
51
  private queryParams;
51
52
  private supabase;
52
53
  private rimoriInfo;
53
- private pluginId;
54
54
  private isMessageChannelReady;
55
55
  private pendingRequests;
56
56
  private updateCallbacks;
@@ -76,4 +76,14 @@ export declare class RimoriCommunicationHandler {
76
76
  * @returns Cleanup function to unregister the callback
77
77
  */
78
78
  onUpdate(callback: (info: RimoriInfo) => void): () => void;
79
+ /**
80
+ * Makes an authenticated fetch request to the Rimori backend.
81
+ * Automatically adds Authorization and plugin-id headers.
82
+ * Content-Type defaults to application/json when the body is a JSON string.
83
+ * Content-Type is omitted for FormData bodies so the browser sets the multipart boundary.
84
+ * Callers can override Content-Type by passing it in options.headers.
85
+ * @param url Path relative to the backend URL (e.g. '/ai/llm')
86
+ * @param options Standard RequestInit options (headers are merged, not replaced)
87
+ */
88
+ fetchBackend(url: string, options?: RequestInit): Promise<Response>;
79
89
  }
@@ -11,6 +11,7 @@ import { PostgrestClient } from '@supabase/postgrest-js';
11
11
  import { EventBus } from '../fromRimori/EventBus';
12
12
  export class RimoriCommunicationHandler {
13
13
  constructor(pluginId, standalone) {
14
+ this.pluginId = pluginId;
14
15
  this.port = null;
15
16
  this.queryParams = {};
16
17
  this.supabase = null;
@@ -18,7 +19,6 @@ export class RimoriCommunicationHandler {
18
19
  this.isMessageChannelReady = false;
19
20
  this.pendingRequests = [];
20
21
  this.updateCallbacks = new Set();
21
- this.pluginId = pluginId;
22
22
  this.getClient = this.getClient.bind(this);
23
23
  //no need to forward messages to parent in standalone mode or worker context
24
24
  if (standalone)
@@ -135,6 +135,7 @@ export class RimoriCommunicationHandler {
135
135
  headers: {
136
136
  apikey: key,
137
137
  Authorization: `Bearer ${token}`,
138
+ 'plugin-id': this.pluginId,
138
139
  },
139
140
  });
140
141
  }
@@ -238,4 +239,25 @@ export class RimoriCommunicationHandler {
238
239
  this.updateCallbacks.delete(callback);
239
240
  };
240
241
  }
242
+ /**
243
+ * Makes an authenticated fetch request to the Rimori backend.
244
+ * Automatically adds Authorization and plugin-id headers.
245
+ * Content-Type defaults to application/json when the body is a JSON string.
246
+ * Content-Type is omitted for FormData bodies so the browser sets the multipart boundary.
247
+ * Callers can override Content-Type by passing it in options.headers.
248
+ * @param url Path relative to the backend URL (e.g. '/ai/llm')
249
+ * @param options Standard RequestInit options (headers are merged, not replaced)
250
+ */
251
+ fetchBackend(url, options = {}) {
252
+ if (!this.rimoriInfo) {
253
+ throw new Error(`[CommunicationHandler:${this.pluginId}] fetchBackend called before rimoriInfo was initialized`);
254
+ }
255
+ const { token, backendUrl } = this.rimoriInfo;
256
+ const defaultContentType = {};
257
+ if (typeof options.body === 'string') {
258
+ defaultContentType['Content-Type'] = 'application/json';
259
+ }
260
+ const headers = Object.assign(Object.assign(Object.assign({}, defaultContentType), options.headers), { Authorization: `Bearer ${token}`, 'plugin-id': this.pluginId });
261
+ return fetch(backendUrl + url, Object.assign(Object.assign({}, options), { headers }));
262
+ }
241
263
  }
@@ -9,6 +9,7 @@ import { StorageModule } from './module/StorageModule';
9
9
  import { EventBusHandler } from '../fromRimori/EventBus';
10
10
  export declare class RimoriClient {
11
11
  private static instance;
12
+ private controller;
12
13
  sharedContent: SharedContentController;
13
14
  db: DbModule;
14
15
  event: EventModule;
@@ -17,7 +18,6 @@ export declare class RimoriClient {
17
18
  exercise: ExerciseModule;
18
19
  /** Upload and manage images stored in Supabase via the backend. */
19
20
  storage: StorageModule;
20
- private rimoriInfo;
21
21
  /** The EventBus instance used by this client. In federation mode this is a per-plugin instance. */
22
22
  eventBus: EventBusHandler;
23
23
  private constructor();
@@ -32,6 +32,6 @@ export declare class RimoriClient {
32
32
  toDashboard: () => void;
33
33
  };
34
34
  runtime: {
35
- fetchBackend: (url: string, options: RequestInit) => Promise<Response>;
35
+ fetchBackend: (url: string, options?: RequestInit) => Promise<Response>;
36
36
  };
37
37
  }
@@ -26,25 +26,20 @@ export class RimoriClient {
26
26
  },
27
27
  };
28
28
  this.runtime = {
29
- fetchBackend: (url, options) => __awaiter(this, void 0, void 0, function* () {
30
- return fetch(this.rimoriInfo.backendUrl + url, Object.assign(Object.assign({}, options), { headers: Object.assign(Object.assign({}, options.headers), { Authorization: `Bearer ${this.rimoriInfo.token}` }) }));
31
- }),
29
+ fetchBackend: (url, options = {}) => this.controller.fetchBackend(url, options),
32
30
  };
33
- this.rimoriInfo = info;
31
+ this.controller = controller;
34
32
  this.eventBus = eventBus !== null && eventBus !== void 0 ? eventBus : EventBus;
35
33
  this.sharedContent = new SharedContentController(supabase, this);
36
- this.ai = new AIModule(info.backendUrl, () => this.rimoriInfo.token, info.pluginId);
34
+ this.ai = new AIModule(controller);
37
35
  this.ai.setOnRateLimited((exercisesRemaining) => {
38
36
  this.eventBus.emit(info.pluginId, 'global.quota.triggerExceeded', { exercises_remaining: exercisesRemaining });
39
37
  });
40
- this.event = new EventModule(info.pluginId, info.backendUrl, () => this.rimoriInfo.token, this.ai, this.eventBus);
38
+ this.event = new EventModule(info.pluginId, this.ai, this.eventBus);
41
39
  this.db = new DbModule(supabase, controller, info);
42
40
  this.plugin = new PluginModule(supabase, controller, info, this.ai);
43
41
  this.exercise = new ExerciseModule(supabase, controller, info, this.event);
44
- this.storage = new StorageModule(info.backendUrl, () => this.rimoriInfo.token);
45
- controller.onUpdate((updatedInfo) => {
46
- this.rimoriInfo = updatedInfo;
47
- });
42
+ this.storage = new StorageModule(controller);
48
43
  //only init logger in workers and on main plugin pages
49
44
  if (this.plugin.applicationMode !== 'sidebar') {
50
45
  Logger.getInstance(this);
@@ -58,11 +53,13 @@ export class RimoriClient {
58
53
  static createWithInfo(info) {
59
54
  const eventBus = EventBusHandler.create('Plugin EventBus ' + info.pluginId);
60
55
  const controller = new RimoriCommunicationHandler(info.pluginId, true);
56
+ controller.handleRimoriInfoUpdate(info);
61
57
  const supabase = new PostgrestClient(`${info.url}/rest/v1`, {
62
58
  schema: info.dbSchema,
63
59
  headers: {
64
60
  apikey: info.key,
65
61
  Authorization: `Bearer ${info.token}`,
62
+ 'plugin-id': info.pluginId,
66
63
  },
67
64
  });
68
65
  const client = new RimoriClient(controller, supabase, info, eventBus);
@@ -1,5 +1,6 @@
1
1
  import { Language } from './PluginModule';
2
2
  import { Tool } from '../../fromRimori/PluginTypes';
3
+ import { RimoriCommunicationHandler } from '../CommunicationHandler';
3
4
  export type OnStreamedObjectResult<T = any> = (result: T, isLoading: boolean) => void;
4
5
  type PrimitiveType = 'string' | 'number' | 'boolean';
5
6
  type ObjectToolParameterType = PrimitiveType | {
@@ -40,12 +41,10 @@ export type OnLLMResponse = (id: string, response: string, finished: boolean, to
40
41
  * Provides access to text generation, voice synthesis, and object generation.
41
42
  */
42
43
  export declare class AIModule {
43
- private getToken;
44
- private backendUrl;
45
- private pluginId;
44
+ private controller;
46
45
  private sessionTokenId;
47
46
  private onRateLimitedCb?;
48
- constructor(backendUrl: string, getToken: () => string, pluginId?: string);
47
+ constructor(controller: RimoriCommunicationHandler);
49
48
  /**
50
49
  * Resolves a prompt name following the event naming convention:
51
50
  * - 2-segment names (e.g. 'storytelling.story') get prefixed with pluginId → '<pluginId>.storytelling.story'
@@ -60,11 +59,6 @@ export declare class AIModule {
60
59
  set: (id: string) => void;
61
60
  /** Clears the stored session token. */
62
61
  clear: () => void;
63
- /**
64
- * Ensures a session token exists, creating one from the backend if needed.
65
- * Mirrors the lazy-issuance pattern used by the AI/LLM endpoint.
66
- */
67
- ensure: () => Promise<void>;
68
62
  };
69
63
  /** Registers a callback invoked whenever a 429 rate-limit response is received. */
70
64
  setOnRateLimited(cb: (exercisesRemaining: number) => void): void;
@@ -109,6 +103,11 @@ export declare class AIModule {
109
103
  * @param language Optional language for the voice.
110
104
  * @param cache Whether to cache the result (default: false).
111
105
  * @returns The generated audio as a Blob.
106
+ *
107
+ * **Empty input:** If `text` is empty or whitespace-only, no network request is
108
+ * made and an empty `Blob` is returned immediately. This prevents a 400 error
109
+ * from the TTS backend while keeping the caller's workflow intact.
110
+ * A warning is logged to the console in this case.
112
111
  */
113
112
  getVoice(text: string, voice?: string, speed?: number, language?: string, cache?: boolean, instructions?: string): Promise<Blob>;
114
113
  /**
@@ -12,7 +12,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
12
12
  * Provides access to text generation, voice synthesis, and object generation.
13
13
  */
14
14
  export class AIModule {
15
- constructor(backendUrl, getToken, pluginId) {
15
+ constructor(controller) {
16
16
  this.sessionTokenId = null;
17
17
  /** Exercise session management. */
18
18
  this.session = {
@@ -26,34 +26,8 @@ export class AIModule {
26
26
  clear: () => {
27
27
  this.sessionTokenId = null;
28
28
  },
29
- /**
30
- * Ensures a session token exists, creating one from the backend if needed.
31
- * Mirrors the lazy-issuance pattern used by the AI/LLM endpoint.
32
- */
33
- ensure: () => __awaiter(this, void 0, void 0, function* () {
34
- var _a, _b, _c;
35
- if (this.sessionTokenId)
36
- return;
37
- const response = yield fetch(`${this.backendUrl}/ai/session`, {
38
- method: 'POST',
39
- headers: { Authorization: `Bearer ${this.getToken()}` },
40
- });
41
- if (!response.ok) {
42
- if (response.status === 429) {
43
- const body = yield response.json().catch(() => ({}));
44
- const remaining = (_a = body.exercises_remaining) !== null && _a !== void 0 ? _a : 0;
45
- (_b = this.onRateLimitedCb) === null || _b === void 0 ? void 0 : _b.call(this, remaining);
46
- throw new Error(`Rate limit exceeded: ${(_c = body.error) !== null && _c !== void 0 ? _c : 'Daily exercise limit reached'}. exercises_remaining: ${remaining}`);
47
- }
48
- throw new Error(`Failed to create session: ${response.status} ${response.statusText}`);
49
- }
50
- const { session_token_id } = yield response.json();
51
- this.sessionTokenId = session_token_id;
52
- }),
53
29
  };
54
- this.backendUrl = backendUrl;
55
- this.getToken = getToken;
56
- this.pluginId = pluginId;
30
+ this.controller = controller;
57
31
  }
58
32
  /**
59
33
  * Resolves a prompt name following the event naming convention:
@@ -64,8 +38,8 @@ export class AIModule {
64
38
  if (name.startsWith('global.'))
65
39
  return name;
66
40
  const segments = name.split('.');
67
- if (segments.length === 2 && this.pluginId) {
68
- return `${this.pluginId}.${name}`;
41
+ if (segments.length === 2 && this.controller.pluginId) {
42
+ return `${this.controller.pluginId}.${name}`;
69
43
  }
70
44
  return name;
71
45
  }
@@ -128,17 +102,21 @@ export class AIModule {
128
102
  * @param language Optional language for the voice.
129
103
  * @param cache Whether to cache the result (default: false).
130
104
  * @returns The generated audio as a Blob.
105
+ *
106
+ * **Empty input:** If `text` is empty or whitespace-only, no network request is
107
+ * made and an empty `Blob` is returned immediately. This prevents a 400 error
108
+ * from the TTS backend while keeping the caller's workflow intact.
109
+ * A warning is logged to the console in this case.
131
110
  */
132
111
  getVoice(text_1) {
133
112
  return __awaiter(this, arguments, void 0, function* (text, voice = 'alloy', speed = 1, language, cache = false, instructions) {
134
113
  var _a;
135
- yield this.session.ensure();
136
- return yield fetch(`${this.backendUrl}/voice/tts`, {
114
+ if (!text.trim().length) {
115
+ console.warn('[rimori-client] getVoice called with empty text — skipping TTS request and returning empty Blob.');
116
+ return new Blob([], { type: 'audio/mpeg' });
117
+ }
118
+ return yield this.controller.fetchBackend('/voice/tts', {
137
119
  method: 'POST',
138
- headers: {
139
- 'Content-Type': 'application/json',
140
- Authorization: `Bearer ${this.getToken()}`,
141
- },
142
120
  body: JSON.stringify({
143
121
  input: text,
144
122
  voice,
@@ -159,7 +137,6 @@ export class AIModule {
159
137
  */
160
138
  getTextFromVoice(file, language) {
161
139
  return __awaiter(this, void 0, void 0, function* () {
162
- yield this.session.ensure();
163
140
  const formData = new FormData();
164
141
  formData.append('file', file);
165
142
  if (language) {
@@ -168,9 +145,8 @@ export class AIModule {
168
145
  if (this.sessionTokenId) {
169
146
  formData.append('session_token_id', this.sessionTokenId);
170
147
  }
171
- return yield fetch(`${this.backendUrl}/voice/stt`, {
148
+ return yield this.controller.fetchBackend('/voice/stt', {
172
149
  method: 'POST',
173
- headers: { Authorization: `Bearer ${this.getToken()}` },
174
150
  body: formData,
175
151
  })
176
152
  .then((r) => r.json())
@@ -236,10 +212,9 @@ export class AIModule {
236
212
  if (prompt) {
237
213
  payload.prompt = { name: this.resolvePromptName(prompt), variables: variables !== null && variables !== void 0 ? variables : {} };
238
214
  }
239
- const response = yield fetch(`${this.backendUrl}/ai/llm`, {
240
- body: JSON.stringify(payload),
215
+ const response = yield this.controller.fetchBackend('/ai/llm', {
241
216
  method: 'POST',
242
- headers: { Authorization: `Bearer ${this.getToken()}`, 'Content-Type': 'application/json' },
217
+ body: JSON.stringify(payload),
243
218
  });
244
219
  if (!response.ok) {
245
220
  if (response.status === 429) {
@@ -361,13 +336,9 @@ export class AIModule {
361
336
  }
362
337
  sendToolResult(toolCallId, result) {
363
338
  return __awaiter(this, void 0, void 0, function* () {
364
- yield fetch(`${this.backendUrl}/ai/llm/tool_result`, {
339
+ yield this.controller.fetchBackend('/ai/llm/tool_result', {
365
340
  method: 'POST',
366
- body: JSON.stringify({
367
- toolCallId,
368
- result: result !== null && result !== void 0 ? result : '[DONE]',
369
- }),
370
- headers: { Authorization: `Bearer ${this.getToken()} `, 'Content-Type': 'application/json' },
341
+ body: JSON.stringify({ toolCallId, result: result !== null && result !== void 0 ? result : '[DONE]' }),
371
342
  });
372
343
  });
373
344
  }
@@ -42,7 +42,7 @@ export type VectorSearchResult<T = Record<string, unknown>> = Array<T & {
42
42
  }>;
43
43
  export declare class DbModule {
44
44
  private supabase;
45
- private rimoriInfo;
45
+ private communicationHandler;
46
46
  tablePrefix: string;
47
47
  schema: string;
48
48
  constructor(supabase: SupabaseClient, communicationHandler: RimoriCommunicationHandler, info: RimoriInfo);
@@ -10,11 +10,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  export class DbModule {
11
11
  constructor(supabase, communicationHandler, info) {
12
12
  this.supabase = supabase;
13
- this.rimoriInfo = info;
13
+ this.communicationHandler = communicationHandler;
14
14
  this.tablePrefix = info.tablePrefix;
15
15
  this.schema = info.dbSchema;
16
16
  communicationHandler.onUpdate((updatedInfo) => {
17
- this.rimoriInfo = updatedInfo;
18
17
  this.tablePrefix = updatedInfo.tablePrefix;
19
18
  this.schema = updatedInfo.dbSchema;
20
19
  });
@@ -37,7 +36,7 @@ export class DbModule {
37
36
  if (relation.startsWith('global_')) {
38
37
  return this.supabase.schema('public').from(tableName);
39
38
  }
40
- return this.supabase.schema(this.rimoriInfo.dbSchema).from(tableName);
39
+ return this.supabase.schema(this.schema).from(tableName);
41
40
  }
42
41
  /**
43
42
  * Get the table name for a given plugin table.
@@ -67,15 +66,11 @@ export class DbModule {
67
66
  setPublicity(table, entryId, publicity) {
68
67
  return __awaiter(this, void 0, void 0, function* () {
69
68
  const tableName = this.getTableName(table);
70
- yield fetch(`${this.rimoriInfo.backendUrl}/db-entry/publicity`, {
69
+ yield this.communicationHandler.fetchBackend('/db-entry/publicity', {
71
70
  method: 'POST',
72
- headers: {
73
- 'Content-Type': 'application/json',
74
- Authorization: `Bearer ${this.rimoriInfo.token}`,
75
- },
76
71
  body: JSON.stringify({
77
72
  table_name: tableName,
78
- schema: this.rimoriInfo.dbSchema,
73
+ schema: this.schema,
79
74
  entry_id: entryId,
80
75
  publicity,
81
76
  }),
@@ -90,12 +85,8 @@ export class DbModule {
90
85
  */
91
86
  vectorSearch(params) {
92
87
  return __awaiter(this, void 0, void 0, function* () {
93
- const response = yield fetch(`${this.rimoriInfo.backendUrl}/plugin-search/vector-search`, {
88
+ const response = yield this.communicationHandler.fetchBackend('/plugin-search/vector-search', {
94
89
  method: 'POST',
95
- headers: {
96
- 'Content-Type': 'application/json',
97
- Authorization: `Bearer ${this.rimoriInfo.token}`,
98
- },
99
90
  body: JSON.stringify(params),
100
91
  });
101
92
  if (!response.ok) {
@@ -10,10 +10,8 @@ export declare class EventModule {
10
10
  private pluginId;
11
11
  private accomplishmentController;
12
12
  private aiModule;
13
- private backendUrl;
14
- private getToken;
15
13
  private eventBus;
16
- constructor(pluginId: string, backendUrl: string, getToken: () => string, aiModule: AIModule, eventBus?: EventBusHandler);
14
+ constructor(pluginId: string, aiModule: AIModule, eventBus?: EventBusHandler);
17
15
  getGlobalEventTopic(preliminaryTopic: string): string;
18
16
  /**
19
17
  * Emit an event to Rimori or a plugin.
@@ -14,13 +14,23 @@ import { EventBus } from '../../fromRimori/EventBus';
14
14
  * Provides methods for emitting, listening to, and responding to events.
15
15
  */
16
16
  export class EventModule {
17
- constructor(pluginId, backendUrl, getToken, aiModule, eventBus) {
17
+ constructor(pluginId, aiModule, eventBus) {
18
18
  this.pluginId = pluginId;
19
- this.backendUrl = backendUrl;
20
- this.getToken = getToken;
21
19
  this.aiModule = aiModule;
22
20
  this.eventBus = eventBus !== null && eventBus !== void 0 ? eventBus : EventBus;
23
21
  this.accomplishmentController = new AccomplishmentController(pluginId, this.eventBus);
22
+ // Listen for session token broadcasts from rimori-main (ExerciseSessionManager).
23
+ // When an exercise starts: adopt the exercise token unconditionally.
24
+ // When an exercise ends (session_token: null): clear the current token.
25
+ // This runs on the raw eventBus to bypass the per-plugin token-gating in on().
26
+ this.eventBus.on(['global.session.triggerUpdate'], (event) => {
27
+ if (event.data.session_token) {
28
+ this.aiModule.session.set(event.data.session_token);
29
+ }
30
+ else {
31
+ this.aiModule.session.clear();
32
+ }
33
+ });
24
34
  }
25
35
  getGlobalEventTopic(preliminaryTopic) {
26
36
  var _a;
@@ -67,7 +77,6 @@ export class EventModule {
67
77
  return __awaiter(this, void 0, void 0, function* () {
68
78
  var _a;
69
79
  const globalTopic = this.getGlobalEventTopic(topic);
70
- yield this.aiModule.session.ensure();
71
80
  const sessionToken = (_a = this.aiModule.session.get()) !== null && _a !== void 0 ? _a : undefined;
72
81
  return this.eventBus.request(this.pluginId, globalTopic, data, sessionToken);
73
82
  });
@@ -133,21 +142,6 @@ export class EventModule {
133
142
  */
134
143
  emitAccomplishment(payload) {
135
144
  return __awaiter(this, void 0, void 0, function* () {
136
- if (payload.type === 'macro') {
137
- const sessionId = this.aiModule.session.get();
138
- if (sessionId) {
139
- try {
140
- yield fetch(`${this.backendUrl}/ai/session/${sessionId}/complete`, {
141
- method: 'PATCH',
142
- headers: { Authorization: `Bearer ${this.getToken()}` },
143
- });
144
- }
145
- catch (_a) {
146
- // non-fatal — session will expire naturally
147
- }
148
- this.aiModule.session.clear();
149
- }
150
- }
151
145
  this.accomplishmentController.emitAccomplishment(payload);
152
146
  });
153
147
  }
@@ -235,6 +229,9 @@ export class EventModule {
235
229
  });
236
230
  console.log('[EventModule] onSidePanelAction: emitting action.requestSidebar for', this.pluginId);
237
231
  this.emit('action.requestSidebar');
232
+ // Bridge is connected at this point — request current session token in case
233
+ // an exercise was already active before this sidebar plugin mounted.
234
+ this.eventBus.emit(this.pluginId, 'global.session.requestCurrent', {});
238
235
  return listener;
239
236
  }
240
237
  }
@@ -35,9 +35,7 @@ export declare class ExerciseModule {
35
35
  private supabase;
36
36
  private communicationHandler;
37
37
  private eventModule;
38
- private backendUrl;
39
- private token;
40
- constructor(supabase: SupabaseClient, communicationHandler: RimoriCommunicationHandler, info: RimoriInfo, eventModule: EventModule);
38
+ constructor(supabase: SupabaseClient, communicationHandler: RimoriCommunicationHandler, _info: RimoriInfo, eventModule: EventModule);
41
39
  /**
42
40
  * Fetches weekly exercises from the weekly_exercises view.
43
41
  * Shows exercises for the current week that haven't expired.
@@ -52,6 +50,22 @@ export declare class ExerciseModule {
52
50
  * @returns Created exercise objects.
53
51
  */
54
52
  add(params: CreateExerciseParams | CreateExerciseParams[]): Promise<Exercise[]>;
53
+ /**
54
+ * Requests a new exercise session token from rimori-main.
55
+ * Use this for self-initiated exercises (user navigated to plugin via navbar and clicked Start).
56
+ * For dashboard-triggered exercises (onMainPanelAction), the token is provided automatically.
57
+ *
58
+ * Emits `global.exercise.triggerStart` and waits for rimori-main to respond with the
59
+ * session token via `global.session.triggerUpdate`. The token is then automatically
60
+ * available for AI calls.
61
+ *
62
+ * @param params.actionKey The action key identifying this exercise type.
63
+ * @param params.knowledgeId Optional knowledge ID for tracking what was studied.
64
+ */
65
+ start(params: {
66
+ actionKey: string;
67
+ knowledgeId?: string;
68
+ }): Promise<void>;
55
69
  /**
56
70
  * Deletes an exercise via the backend API.
57
71
  * @param id The exercise ID to delete.
@@ -12,15 +12,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
12
12
  * Provides access to weekly exercises and exercise management.
13
13
  */
14
14
  export class ExerciseModule {
15
- constructor(supabase, communicationHandler, info, eventModule) {
15
+ constructor(supabase, communicationHandler, _info, eventModule) {
16
16
  this.supabase = supabase;
17
17
  this.communicationHandler = communicationHandler;
18
18
  this.eventModule = eventModule;
19
- this.token = info.token;
20
- this.backendUrl = info.backendUrl;
21
- this.communicationHandler.onUpdate((updatedInfo) => {
22
- this.token = updatedInfo.token;
23
- });
24
19
  }
25
20
  /**
26
21
  * Fetches weekly exercises from the weekly_exercises view.
@@ -46,12 +41,8 @@ export class ExerciseModule {
46
41
  add(params) {
47
42
  return __awaiter(this, void 0, void 0, function* () {
48
43
  const exercises = Array.isArray(params) ? params : [params];
49
- const response = yield fetch(`${this.backendUrl}/exercises`, {
44
+ const response = yield this.communicationHandler.fetchBackend('/exercises', {
50
45
  method: 'POST',
51
- headers: {
52
- 'Content-Type': 'application/json',
53
- Authorization: `Bearer ${this.token}`,
54
- },
55
46
  body: JSON.stringify({ exercises }),
56
47
  });
57
48
  if (!response.ok) {
@@ -63,6 +54,36 @@ export class ExerciseModule {
63
54
  return data;
64
55
  });
65
56
  }
57
+ /**
58
+ * Requests a new exercise session token from rimori-main.
59
+ * Use this for self-initiated exercises (user navigated to plugin via navbar and clicked Start).
60
+ * For dashboard-triggered exercises (onMainPanelAction), the token is provided automatically.
61
+ *
62
+ * Emits `global.exercise.triggerStart` and waits for rimori-main to respond with the
63
+ * session token via `global.session.triggerUpdate`. The token is then automatically
64
+ * available for AI calls.
65
+ *
66
+ * @param params.actionKey The action key identifying this exercise type.
67
+ * @param params.knowledgeId Optional knowledge ID for tracking what was studied.
68
+ */
69
+ start(params) {
70
+ return __awaiter(this, void 0, void 0, function* () {
71
+ return new Promise((resolve, reject) => {
72
+ const timeout = setTimeout(() => {
73
+ listener.off();
74
+ reject(new Error('Exercise start timed out: rimori-main did not respond within 5s'));
75
+ }, 5000);
76
+ const listener = this.eventModule.on('global.session.triggerUpdate', ({ data }) => {
77
+ if (data.session_token) {
78
+ clearTimeout(timeout);
79
+ listener.off();
80
+ resolve();
81
+ }
82
+ });
83
+ this.eventModule.emit('global.exercise.triggerStart', params);
84
+ });
85
+ });
86
+ }
66
87
  /**
67
88
  * Deletes an exercise via the backend API.
68
89
  * @param id The exercise ID to delete.
@@ -70,11 +91,8 @@ export class ExerciseModule {
70
91
  */
71
92
  delete(id) {
72
93
  return __awaiter(this, void 0, void 0, function* () {
73
- const response = yield fetch(`${this.backendUrl}/exercises/${id}`, {
94
+ const response = yield this.communicationHandler.fetchBackend(`/exercises/${id}`, {
74
95
  method: 'DELETE',
75
- headers: {
76
- Authorization: `Bearer ${this.token}`,
77
- },
78
96
  });
79
97
  if (!response.ok) {
80
98
  const errorText = yield response.text();
@@ -34,7 +34,6 @@ export class SharedContentController {
34
34
  // Generate new content via backend endpoint
35
35
  const response = yield this.rimoriClient.runtime.fetchBackend('/shared-content/generate', {
36
36
  method: 'POST',
37
- headers: { 'Content-Type': 'application/json' },
38
37
  body: JSON.stringify({
39
38
  tableName: params.table,
40
39
  skillType: params.skillType,
@@ -67,7 +66,6 @@ export class SharedContentController {
67
66
  return __awaiter(this, arguments, void 0, function* (tableName, topic, limit = 10) {
68
67
  const response = yield this.rimoriClient.runtime.fetchBackend('/shared-content/get-by-topic', {
69
68
  method: 'POST',
70
- headers: { 'Content-Type': 'application/json' },
71
69
  body: JSON.stringify({
72
70
  tableName,
73
71
  limit,
@@ -285,7 +283,6 @@ export class SharedContentController {
285
283
  return __awaiter(this, void 0, void 0, function* () {
286
284
  const response = yield this.rimoriClient.runtime.fetchBackend('/shared-content/update', {
287
285
  method: 'POST',
288
- headers: { 'Content-Type': 'application/json' },
289
286
  body: JSON.stringify({
290
287
  tableName,
291
288
  contentId,
@@ -311,7 +308,6 @@ export class SharedContentController {
311
308
  return __awaiter(this, void 0, void 0, function* () {
312
309
  const response = yield this.rimoriClient.runtime.fetchBackend('/shared-content/validate', {
313
310
  method: 'POST',
314
- headers: { 'Content-Type': 'application/json' },
315
311
  body: JSON.stringify({
316
312
  tableName,
317
313
  contentId,
@@ -8,10 +8,10 @@
8
8
  * confirms images found in content, and deletes orphaned ones. No plugin-side
9
9
  * confirm or delete calls are needed.
10
10
  */
11
+ import { RimoriCommunicationHandler } from '../CommunicationHandler';
11
12
  export declare class StorageModule {
12
- private readonly backendUrl;
13
- private readonly getToken;
14
- constructor(backendUrl: string, getToken: () => string);
13
+ private readonly controller;
14
+ constructor(controller: RimoriCommunicationHandler);
15
15
  /**
16
16
  * Upload a PNG image blob to Supabase storage via the backend.
17
17
  *
@@ -7,20 +7,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- /**
11
- * Storage module for plugin image operations.
12
- *
13
- * Handles uploading images to Supabase storage via the backend.
14
- *
15
- * Images are tracked automatically: the backend cron scans markdown columns
16
- * (declared via `type: 'markdown'` in db.config.ts) every 30 minutes,
17
- * confirms images found in content, and deletes orphaned ones. No plugin-side
18
- * confirm or delete calls are needed.
19
- */
20
10
  export class StorageModule {
21
- constructor(backendUrl, getToken) {
22
- this.backendUrl = backendUrl;
23
- this.getToken = getToken;
11
+ constructor(controller) {
12
+ this.controller = controller;
24
13
  }
25
14
  /**
26
15
  * Upload a PNG image blob to Supabase storage via the backend.
@@ -37,9 +26,8 @@ export class StorageModule {
37
26
  const formData = new FormData();
38
27
  formData.append('file', pngBlob, 'image.png');
39
28
  try {
40
- const response = yield fetch(`${this.backendUrl}/plugin-images/upload`, {
29
+ const response = yield this.controller.fetchBackend('/plugin-images/upload', {
41
30
  method: 'POST',
42
- headers: { Authorization: `Bearer ${this.getToken()}` },
43
31
  body: formData,
44
32
  });
45
33
  if (!response.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/client",
3
- "version": "2.5.31",
3
+ "version": "2.5.32-next.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {