@rimori/client 2.5.19-next.0 → 2.5.19-next.1

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.
@@ -13,6 +13,8 @@ export interface EventBusMessage<T = EventPayload> {
13
13
  topic: string;
14
14
  data: T;
15
15
  debug: boolean;
16
+ /** Session token inherited from the calling plugin, used to attribute AI calls to the originating exercise session. */
17
+ ai_session_token?: string;
16
18
  }
17
19
  export type EventHandler<T = EventPayload> = (event: EventBusMessage<T>) => void | Promise<void>;
18
20
  export interface EventListener {
@@ -56,7 +58,7 @@ export declare class EventBusHandler {
56
58
  * - pl1234.card.delete
57
59
  * - pl1234.card.triggerBackup
58
60
  */
59
- emit<T = EventPayload>(sender: string, topic: string, data?: T, eventId?: number): void;
61
+ emit<T = EventPayload>(sender: string, topic: string, data?: T, eventId?: number, aiSessionToken?: string): void;
60
62
  private emitInternal;
61
63
  /**
62
64
  * Subscribes to an event on the event bus.
@@ -93,7 +95,7 @@ export declare class EventBusHandler {
93
95
  * @param data - The data of the event.
94
96
  * @returns A promise that resolves to the event.
95
97
  */
96
- request<T = EventPayload>(sender: string, topic: string, data?: EventPayload): Promise<EventBusMessage<T>>;
98
+ request<T = EventPayload>(sender: string, topic: string, data?: EventPayload, aiSessionToken?: string): Promise<EventBusMessage<T>>;
97
99
  /**
98
100
  * Gets the matching handlers for an event.
99
101
  * @param topic - The topic of the event.
@@ -63,7 +63,7 @@ export class EventBusHandler {
63
63
  this.generatedIds.set(id, now);
64
64
  return id;
65
65
  }
66
- createEvent(sender, topic, data, eventId) {
66
+ createEvent(sender, topic, data, eventId, aiSessionToken) {
67
67
  const generatedEventId = eventId || this.generateUniqueId();
68
68
  return {
69
69
  eventId: generatedEventId,
@@ -72,6 +72,7 @@ export class EventBusHandler {
72
72
  topic,
73
73
  data,
74
74
  debug: this.debugEnabled,
75
+ ai_session_token: aiSessionToken,
75
76
  };
76
77
  }
77
78
  /**
@@ -92,15 +93,15 @@ export class EventBusHandler {
92
93
  * - pl1234.card.delete
93
94
  * - pl1234.card.triggerBackup
94
95
  */
95
- emit(sender, topic, data, eventId) {
96
- this.emitInternal(sender, topic, data || {}, eventId);
96
+ emit(sender, topic, data, eventId, aiSessionToken) {
97
+ this.emitInternal(sender, topic, data || {}, eventId, false, aiSessionToken);
97
98
  }
98
- emitInternal(sender, topic, data, eventId, skipResponseTrigger = false) {
99
+ emitInternal(sender, topic, data, eventId, skipResponseTrigger = false, aiSessionToken) {
99
100
  if (!this.validateTopic(topic)) {
100
101
  this.logAndThrowError(false, `Invalid topic: ` + topic);
101
102
  return;
102
103
  }
103
- const event = this.createEvent(sender, topic, data, eventId);
104
+ const event = this.createEvent(sender, topic, data, eventId, aiSessionToken);
104
105
  const handlers = this.getMatchingHandlers(event.topic);
105
106
  handlers.forEach((handler) => {
106
107
  if (handler.ignoreSender && handler.ignoreSender.includes(sender)) {
@@ -245,16 +246,16 @@ export class EventBusHandler {
245
246
  * @param data - The data of the event.
246
247
  * @returns A promise that resolves to the event.
247
248
  */
248
- request(sender, topic, data) {
249
+ request(sender, topic, data, aiSessionToken) {
249
250
  return __awaiter(this, void 0, void 0, function* () {
250
251
  if (!this.validateTopic(topic)) {
251
252
  this.logAndThrowError(true, `Invalid topic: ` + topic);
252
253
  }
253
- const event = this.createEvent(sender, topic, data || {});
254
+ const event = this.createEvent(sender, topic, data || {}, undefined, aiSessionToken);
254
255
  this.logIfDebug(`Requesting data from ` + topic, { event });
255
256
  return new Promise((resolve) => {
256
257
  this.responseResolvers.set(event.eventId, (value) => resolve(value));
257
- this.emitInternal(sender, topic, data || {}, event.eventId, true);
258
+ this.emitInternal(sender, topic, data || {}, event.eventId, true, aiSessionToken);
258
259
  });
259
260
  });
260
261
  }
package/dist/index.d.ts CHANGED
@@ -15,7 +15,8 @@ export { Translator } from './controller/TranslationController';
15
15
  export type { TriggerAction } from './plugin/module/ExerciseModule';
16
16
  export type { Message, ToolInvocation } from './plugin/module/AIModule';
17
17
  export type { Theme, ApplicationMode } from './plugin/module/PluginModule';
18
- export type { UserInfo, Language, UserRole, ExplicitUndefined, BasePluginSettings } from './plugin/module/PluginModule';
18
+ export type { UserInfo, Language, UserRole, SubscriptionTier, ExplicitUndefined, BasePluginSettings } from './plugin/module/PluginModule';
19
+ export { TIER_ORDER, ROLE_ORDER } from './plugin/module/PluginModule';
19
20
  export type { SharedContent, BasicSharedContent, ContentStatus } from './plugin/module/SharedContentController';
20
21
  export type { MacroAccomplishmentPayload, MicroAccomplishmentPayload } from './controller/AccomplishmentController';
21
22
  export { StorageModule } from './plugin/module/StorageModule';
package/dist/index.js CHANGED
@@ -10,4 +10,5 @@ export * from './plugin/CommunicationHandler';
10
10
  export { setupWorker } from './worker/WorkerSetup';
11
11
  export { AudioController } from './controller/AudioController';
12
12
  export { Translator } from './controller/TranslationController';
13
+ export { TIER_ORDER, ROLE_ORDER } from './plugin/module/PluginModule';
13
14
  export { StorageModule } from './plugin/module/StorageModule';
@@ -58,9 +58,9 @@ export class RimoriCommunicationHandler {
58
58
  EventBus.emit(this.pluginId, 'error', { error }, eventId);
59
59
  }
60
60
  else if (event) {
61
- const { topic, sender, data: eventData, eventId } = event;
61
+ const { topic, sender, data: eventData, eventId, ai_session_token } = event;
62
62
  if (sender !== this.pluginId) {
63
- EventBus.emit(sender, topic, eventData, eventId);
63
+ EventBus.emit(sender, topic, eventData, eventId, ai_session_token);
64
64
  }
65
65
  else {
66
66
  console.log('[PluginController] event from self', event);
@@ -61,17 +61,20 @@ export declare class AIModule {
61
61
  private sessionTokenId;
62
62
  private onRateLimitedCb?;
63
63
  constructor(backendUrl: string, getToken: () => string);
64
- /** Returns the current exercise session token ID (null if no active session). */
65
- getSessionTokenId(): string | null;
66
- /** Sets the session token ID (used by workers inheriting a session from the plugin). */
67
- setSessionToken(id: string): void;
68
- /** Clears the stored session token (called after macro accomplishment). */
69
- clearSessionToken(): void;
70
- /**
71
- * Ensures a session token exists, requesting one from the backend if needed.
72
- * Mirrors the lazy-issuance pattern used by the AI/LLM endpoint.
73
- */
74
- private ensureSessionToken;
64
+ /** Exercise session management. */
65
+ readonly session: {
66
+ /** Returns the current exercise session token ID (null if no active session). */
67
+ get: () => string | null;
68
+ /** Sets the session token ID. */
69
+ set: (id: string) => void;
70
+ /** Clears the stored session token. */
71
+ clear: () => void;
72
+ /**
73
+ * Ensures a session token exists, creating one from the backend if needed.
74
+ * Mirrors the lazy-issuance pattern used by the AI/LLM endpoint.
75
+ */
76
+ ensure: () => Promise<void>;
77
+ };
75
78
  /** Registers a callback invoked whenever a 429 rate-limit response is received. */
76
79
  setOnRateLimited(cb: (exercisesRemaining: number) => void): void;
77
80
  /**
@@ -14,47 +14,46 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
14
14
  export class AIModule {
15
15
  constructor(backendUrl, getToken) {
16
16
  this.sessionTokenId = null;
17
+ /** Exercise session management. */
18
+ this.session = {
19
+ /** Returns the current exercise session token ID (null if no active session). */
20
+ get: () => this.sessionTokenId,
21
+ /** Sets the session token ID. */
22
+ set: (id) => {
23
+ this.sessionTokenId = id;
24
+ },
25
+ /** Clears the stored session token. */
26
+ clear: () => {
27
+ this.sessionTokenId = null;
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
+ };
17
54
  this.backendUrl = backendUrl;
18
55
  this.getToken = getToken;
19
56
  }
20
- /** Returns the current exercise session token ID (null if no active session). */
21
- getSessionTokenId() {
22
- return this.sessionTokenId;
23
- }
24
- /** Sets the session token ID (used by workers inheriting a session from the plugin). */
25
- setSessionToken(id) {
26
- this.sessionTokenId = id;
27
- }
28
- /** Clears the stored session token (called after macro accomplishment). */
29
- clearSessionToken() {
30
- this.sessionTokenId = null;
31
- }
32
- /**
33
- * Ensures a session token exists, requesting one from the backend if needed.
34
- * Mirrors the lazy-issuance pattern used by the AI/LLM endpoint.
35
- */
36
- ensureSessionToken() {
37
- return __awaiter(this, void 0, void 0, function* () {
38
- var _a, _b, _c;
39
- if (this.sessionTokenId)
40
- return;
41
- const response = yield fetch(`${this.backendUrl}/ai/session`, {
42
- method: 'POST',
43
- headers: { Authorization: `Bearer ${this.getToken()}` },
44
- });
45
- if (!response.ok) {
46
- if (response.status === 429) {
47
- const body = yield response.json().catch(() => ({}));
48
- const remaining = (_a = body.exercises_remaining) !== null && _a !== void 0 ? _a : 0;
49
- (_b = this.onRateLimitedCb) === null || _b === void 0 ? void 0 : _b.call(this, remaining);
50
- throw new Error(`Rate limit exceeded: ${(_c = body.error) !== null && _c !== void 0 ? _c : 'Daily exercise limit reached'}. exercises_remaining: ${remaining}`);
51
- }
52
- throw new Error(`Failed to create session: ${response.status} ${response.statusText}`);
53
- }
54
- const { session_token_id } = yield response.json();
55
- this.sessionTokenId = session_token_id;
56
- });
57
- }
58
57
  /** Registers a callback invoked whenever a 429 rate-limit response is received. */
59
58
  setOnRateLimited(cb) {
60
59
  this.onRateLimitedCb = cb;
@@ -123,7 +122,7 @@ export class AIModule {
123
122
  getVoice(text_1) {
124
123
  return __awaiter(this, arguments, void 0, function* (text, voice = 'alloy', speed = 1, language, cache = false) {
125
124
  var _a;
126
- yield this.ensureSessionToken();
125
+ yield this.session.ensure();
127
126
  return yield fetch(`${this.backendUrl}/voice/tts`, {
128
127
  method: 'POST',
129
128
  headers: {
@@ -142,7 +141,7 @@ export class AIModule {
142
141
  */
143
142
  getTextFromVoice(file, language) {
144
143
  return __awaiter(this, void 0, void 0, function* () {
145
- yield this.ensureSessionToken();
144
+ yield this.session.ensure();
146
145
  const formData = new FormData();
147
146
  formData.append('file', file);
148
147
  if (language) {
@@ -1,7 +1,5 @@
1
- import { PostgrestClientOptions, PostgrestQueryBuilder } from '@supabase/postgrest-js';
2
1
  import { SupabaseClient } from '../CommunicationHandler';
3
2
  import { RimoriCommunicationHandler, RimoriInfo } from '../CommunicationHandler';
4
- import { GenericSchema, GenericTable } from '@supabase/postgrest-js/dist/cjs/types/common/common';
5
3
  /**
6
4
  * Database module for plugin database operations.
7
5
  * Provides access to plugin tables with automatic prefixing and schema management.
@@ -19,7 +17,7 @@ export declare class DbModule {
19
17
  * @param relation The table name (without prefix for plugin tables, with 'global_' for global tables).
20
18
  * @returns A Postgrest query builder for the table.
21
19
  */
22
- from<ViewName extends string & keyof GenericSchema['Views'], View extends GenericSchema['Views'][ViewName]>(relation: string): PostgrestQueryBuilder<PostgrestClientOptions, GenericSchema, GenericTable, ViewName, View>;
20
+ from(relation: string): import("@supabase/postgrest-js").PostgrestQueryBuilder<{} | import("@supabase/postgrest-js").PostgrestClientOptions, any, any, string, unknown>;
23
21
  /**
24
22
  * Get the table name for a given plugin table.
25
23
  * Internally all tables are prefixed with the plugin id. This function is used to get the correct table name for a given public table.
@@ -23,14 +23,9 @@ export class DbModule {
23
23
  */
24
24
  from(relation) {
25
25
  const tableName = this.getTableName(relation);
26
- // Use the schema determined by rimori-main based on release channel
27
- // Global tables (starting with 'global_') remain in public schema
28
- // Plugin tables use the schema provided by rimori-main (plugins or plugins_alpha)
29
26
  if (relation.startsWith('global_')) {
30
- // Global tables stay in public schema
31
27
  return this.supabase.schema('public').from(tableName);
32
28
  }
33
- // Plugin tables go to the schema provided by rimori-main
34
29
  return this.supabase.schema(this.rimoriInfo.dbSchema).from(tableName);
35
30
  }
36
31
  /**
@@ -63,8 +63,13 @@ export class EventModule {
63
63
  * @returns The response from the event.
64
64
  */
65
65
  request(topic, data) {
66
- const globalTopic = this.getGlobalEventTopic(topic);
67
- return EventBus.request(this.pluginId, globalTopic, data);
66
+ return __awaiter(this, void 0, void 0, function* () {
67
+ var _a;
68
+ const globalTopic = this.getGlobalEventTopic(topic);
69
+ yield this.aiModule.session.ensure();
70
+ const sessionToken = (_a = this.aiModule.session.get()) !== null && _a !== void 0 ? _a : undefined;
71
+ return EventBus.request(this.pluginId, globalTopic, data, sessionToken);
72
+ });
68
73
  }
69
74
  /**
70
75
  * Subscribe to an event.
@@ -74,7 +79,12 @@ export class EventModule {
74
79
  */
75
80
  on(topic, callback) {
76
81
  const topics = Array.isArray(topic) ? topic : [topic];
77
- return EventBus.on(topics.map((t) => this.getGlobalEventTopic(t)), callback);
82
+ return EventBus.on(topics.map((t) => this.getGlobalEventTopic(t)), (event) => {
83
+ if (event.ai_session_token && !this.aiModule.session.get()) {
84
+ this.aiModule.session.set(event.ai_session_token);
85
+ }
86
+ callback(event);
87
+ });
78
88
  }
79
89
  /**
80
90
  * Subscribe to an event once.
@@ -91,7 +101,30 @@ export class EventModule {
91
101
  */
92
102
  respond(topic, data) {
93
103
  const topics = Array.isArray(topic) ? topic : [topic];
94
- EventBus.respond(this.pluginId, topics.map((t) => this.getGlobalEventTopic(t)), data);
104
+ let wrappedData = data;
105
+ if (typeof data === 'function') {
106
+ wrappedData = (event) => __awaiter(this, void 0, void 0, function* () {
107
+ const previousToken = this.aiModule.session.get();
108
+ if (event.ai_session_token) {
109
+ this.aiModule.session.set(event.ai_session_token);
110
+ }
111
+ try {
112
+ console.log('responding to event', event);
113
+ return yield data(event);
114
+ }
115
+ finally {
116
+ if (event.ai_session_token) {
117
+ if (previousToken) {
118
+ this.aiModule.session.set(previousToken);
119
+ }
120
+ else {
121
+ this.aiModule.session.clear();
122
+ }
123
+ }
124
+ }
125
+ });
126
+ }
127
+ EventBus.respond(this.pluginId, topics.map((t) => this.getGlobalEventTopic(t)), wrappedData);
95
128
  }
96
129
  /**
97
130
  * Emit an accomplishment.
@@ -100,7 +133,7 @@ export class EventModule {
100
133
  emitAccomplishment(payload) {
101
134
  return __awaiter(this, void 0, void 0, function* () {
102
135
  if (payload.type === 'macro') {
103
- const sessionId = this.aiModule.getSessionTokenId();
136
+ const sessionId = this.aiModule.session.get();
104
137
  if (sessionId) {
105
138
  try {
106
139
  yield fetch(`${this.backendUrl}/ai/session/${sessionId}/complete`, {
@@ -111,7 +144,7 @@ export class EventModule {
111
144
  catch (_a) {
112
145
  // non-fatal — session will expire naturally
113
146
  }
114
- this.aiModule.clearSessionToken();
147
+ this.aiModule.session.clear();
115
148
  }
116
149
  }
117
150
  this.accomplishmentController.emitAccomplishment(payload);
@@ -105,6 +105,11 @@ export interface Language {
105
105
  uppercase: string;
106
106
  }
107
107
  export type UserRole = 'user' | 'plugin_moderator' | 'lang_moderator' | 'admin';
108
+ export type SubscriptionTier = 'anonymous' | 'free' | 'standard' | 'premium' | 'early_access';
109
+ /** Ordered tiers from lowest to highest access level */
110
+ export declare const TIER_ORDER: SubscriptionTier[];
111
+ /** Ordered roles from lowest to highest access level */
112
+ export declare const ROLE_ORDER: UserRole[];
108
113
  export declare const LEARNING_REASONS: readonly ["work", "partner", "friends", "study", "living", "culture", "growth", "citizenship", "other"];
109
114
  export type LearningReason = (typeof LEARNING_REASONS)[number];
110
115
  export type ExplicitUndefined<T> = {
@@ -161,4 +166,9 @@ export interface UserInfo {
161
166
  * The user's role: 'user', 'plugin_moderator', 'lang_moderator', or 'admin'
162
167
  */
163
168
  user_role: UserRole;
169
+ /**
170
+ * The user's subscription tier. Higher tiers include all privileges of lower tiers.
171
+ * Order (ascending): anonymous < free < standard < premium < early_access
172
+ */
173
+ subscription_tier: SubscriptionTier;
164
174
  }
@@ -178,6 +178,10 @@ export class PluginModule {
178
178
  });
179
179
  }
180
180
  }
181
+ /** Ordered tiers from lowest to highest access level */
182
+ export const TIER_ORDER = ['anonymous', 'free', 'standard', 'premium', 'early_access'];
183
+ /** Ordered roles from lowest to highest access level */
184
+ export const ROLE_ORDER = ['user', 'plugin_moderator', 'lang_moderator', 'admin'];
181
185
  export const LEARNING_REASONS = [
182
186
  'work',
183
187
  'partner',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/client",
3
- "version": "2.5.19-next.0",
3
+ "version": "2.5.19-next.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {