@rimori/client 2.5.37 → 2.5.38-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.
package/README.md CHANGED
@@ -38,7 +38,7 @@ The `@rimori/client` package is the framework-agnostic runtime and CLI that powe
38
38
  ```bash
39
39
  npm install @rimori/client
40
40
  # or
41
- yarn add @rimori/client
41
+ pnpm add @rimori/client
42
42
  ```
43
43
 
44
44
  ## Relationship to @rimori/react-client
@@ -97,7 +97,7 @@ npx @rimori/client rimori-init --upgrade # refresh config without changing the
97
97
  Usage:
98
98
 
99
99
  ```bash
100
- yarn build
100
+ pnpm build
101
101
  npx @rimori/client rimori-release alpha
102
102
  ```
103
103
 
@@ -132,11 +132,11 @@ async function main() {
132
132
  console.log('1. Check out ./rimori/readme.md for more information about how to make the most out of the plugin.');
133
133
  console.log('2. Adapt the ./rimori/rimori.config.ts file to your needs.');
134
134
  console.log('3. Under ./public/docs/ you can find the documentation for an example flashcard plugin to get started easier.');
135
- console.log('4. Start development with: yarn dev');
135
+ console.log('4. Start development with: pnpm dev');
136
136
  console.log('');
137
137
  console.log(`The plugin should now be accessible at: http://localhost:${3000}`);
138
138
  console.log('');
139
- console.log('If you want to release the plugin, simply run: "yarn release:<alpha|beta|stable>" (details are available in ./rimori/readme.md)');
139
+ console.log('If you want to release the plugin, simply run: "pnpm release:<alpha|beta|stable>" (details are available in ./rimori/readme.md)');
140
140
  }
141
141
  catch (error) {
142
142
  console.error(`❌ Error: ${error instanceof Error ? error.message : error}`);
@@ -65,11 +65,11 @@ export function updatePackageJson({ pluginId, port, isUpgrade = false }) {
65
65
  packageJson.scripts = {
66
66
  ...packageJson.scripts,
67
67
  dev: `vite --port ${port || 3000}`,
68
- build: 'yarn run check && vite build',
68
+ build: 'pnpm check && vite build',
69
69
  check: 'tsc --project tsconfig.app.json --noEmit --pretty',
70
- 'release:alpha': 'yarn build && yarn rimori-release alpha',
71
- 'release:beta': 'yarn build && yarn rimori-release beta',
72
- 'release:stable': 'yarn build && yarn rimori-release stable',
70
+ 'release:alpha': 'pnpm build && pnpm rimori-release alpha',
71
+ 'release:beta': 'pnpm build && pnpm rimori-release beta',
72
+ 'release:stable': 'pnpm build && pnpm rimori-release stable',
73
73
  'dev:worker': 'VITE_MINIFY=false vite build --watch --config worker/vite.config.ts',
74
74
  'build:worker': 'vite build --config worker/vite.config.ts',
75
75
  };
@@ -56,6 +56,9 @@ export async function sendConfiguration(config) {
56
56
  rimori_client_version: config.rimori_client_version,
57
57
  provided_languages: availableLanguages.join(','),
58
58
  };
59
+ if (config.dev_sync) {
60
+ requestBody.update_existing = true;
61
+ }
59
62
  try {
60
63
  const response = await fetch(`${config.domain}/release`, {
61
64
  method: 'POST',
@@ -18,6 +18,7 @@ declare const config: {
18
18
  token: string;
19
19
  domain: string;
20
20
  rimori_client_version: any;
21
+ dev_sync: boolean;
21
22
  };
22
23
  export type Config = typeof config;
23
24
  export {};
@@ -29,9 +29,15 @@ if (!pluginId) {
29
29
  console.error('Error: The plugin id (r_id) is not set in package.json');
30
30
  process.exit(1);
31
31
  }
32
- const [releaseChannel] = process.argv.slice(2);
32
+ const cliArgs = process.argv.slice(2);
33
+ const devSync = cliArgs.includes('--dev-sync');
34
+ const releaseChannel = cliArgs.find((a) => !a.startsWith('--'));
33
35
  if (!releaseChannel) {
34
- console.error('Usage: rimori-release <release_channel>');
36
+ console.error('Usage: rimori-release <release_channel> [--dev-sync]');
37
+ process.exit(1);
38
+ }
39
+ if (devSync && releaseChannel !== 'alpha') {
40
+ console.error('--dev-sync is only allowed with the alpha channel');
35
41
  process.exit(1);
36
42
  }
37
43
  const config = {
@@ -41,19 +47,30 @@ const config = {
41
47
  token: RIMORI_TOKEN,
42
48
  domain: process.env.RIMORI_BACKEND_URL || 'https://api.rimori.se',
43
49
  rimori_client_version: packageJson.dependencies['@rimori/client'].replace('^', ''),
50
+ dev_sync: devSync,
44
51
  };
45
52
  /**
46
53
  * Main release process
47
54
  */
48
55
  async function releaseProcess() {
49
56
  try {
50
- console.log(`🚀 Releasing ${config.plugin_id} to ${config.release_channel}...`);
57
+ if (config.dev_sync) {
58
+ console.log(`⚡ Dev-sync ${config.plugin_id} → existing alpha release`);
59
+ }
60
+ else {
61
+ console.log(`🚀 Releasing ${config.plugin_id} to ${config.release_channel}...`);
62
+ }
51
63
  console.log(`📡 Deploying to: ${config.domain}`);
52
64
  // First send the configuration
53
65
  const release_id = await sendConfiguration(config);
54
66
  // Upload prompts (if prompts.config.ts exists)
55
67
  await promptsUpload(config, release_id);
56
68
  await dbUpdate(config, release_id);
69
+ // Dev-sync only pushes metadata — skip bundle upload and finalize.
70
+ if (config.dev_sync) {
71
+ console.log('✅ Dev-sync complete');
72
+ return;
73
+ }
57
74
  // Then upload the files
58
75
  await uploadDirectory(config, release_id);
59
76
  // Then release the plugin
@@ -16,7 +16,12 @@
16
16
  * 3. The column MUST be named 'embedding' and only one per table is allowed.
17
17
  * 4. Requires `source_column` — the column whose content is embedded async on insert.
18
18
  */
19
- type DbColumnType = 'decimal' | 'integer' | 'text' | 'boolean' | 'json' | 'timestamp' | 'uuid' | 'markdown' | 'vector';
19
+ /**
20
+ * 'image', 'audio', 'video' and 'file' are stored as `text` (URL) in the database.
21
+ * The migration system adds an `updated_at` trigger and registers the column for
22
+ * the asset-refs cron, which links rows to bucket files and deletes orphans.
23
+ */
24
+ type DbColumnType = 'decimal' | 'integer' | 'text' | 'boolean' | 'json' | 'timestamp' | 'uuid' | 'markdown' | 'vector' | 'image' | 'audio' | 'video' | 'file';
20
25
  /**
21
26
  * Foreign key relationship configuration with cascade delete support.
22
27
  * Defines a relationship where the source record is deleted when the destination record is deleted.
package/dist/index.d.ts CHANGED
@@ -21,4 +21,5 @@ export { TIER_ORDER, ROLE_ORDER } from './plugin/module/PluginModule';
21
21
  export type { SharedContent, BasicSharedContent, ContentStatus } from './plugin/module/SharedContentController';
22
22
  export type { MacroAccomplishmentPayload, MicroAccomplishmentPayload } from './controller/AccomplishmentController';
23
23
  export { StorageModule } from './plugin/module/StorageModule';
24
+ export { AssetsModule, type AssetKind } from './plugin/module/AssetsModule';
24
25
  export type { PublicityLevel } from './plugin/module/DbModule';
package/dist/index.js CHANGED
@@ -13,3 +13,4 @@ export { AudioController } from './controller/AudioController';
13
13
  export { Translator } from './controller/TranslationController';
14
14
  export { TIER_ORDER, ROLE_ORDER } from './plugin/module/PluginModule';
15
15
  export { StorageModule } from './plugin/module/StorageModule';
16
+ export { AssetsModule } from './plugin/module/AssetsModule';
@@ -6,6 +6,7 @@ import { EventModule } from './module/EventModule';
6
6
  import { AIModule } from './module/AIModule';
7
7
  import { ExerciseModule } from './module/ExerciseModule';
8
8
  import { StorageModule } from './module/StorageModule';
9
+ import { AssetsModule } from './module/AssetsModule';
9
10
  import { EventBusHandler } from '../fromRimori/EventBus';
10
11
  export declare class RimoriClient {
11
12
  private static instance;
@@ -18,6 +19,8 @@ export declare class RimoriClient {
18
19
  exercise: ExerciseModule;
19
20
  /** Upload and manage images stored in Supabase via the backend. */
20
21
  storage: StorageModule;
22
+ /** Upload assets (image/audio/video/file) backing asset-typed db.config columns. */
23
+ assets: AssetsModule;
21
24
  /** The EventBus instance used by this client. In federation mode this is a per-plugin instance. */
22
25
  eventBus: EventBusHandler;
23
26
  private constructor();
@@ -7,6 +7,7 @@ import { EventModule } from './module/EventModule';
7
7
  import { AIModule } from './module/AIModule';
8
8
  import { ExerciseModule } from './module/ExerciseModule';
9
9
  import { StorageModule } from './module/StorageModule';
10
+ import { AssetsModule } from './module/AssetsModule';
10
11
  import { PostgrestClient } from '@supabase/postgrest-js';
11
12
  import { EventBus, EventBusHandler } from '../fromRimori/EventBus';
12
13
  export class RimoriClient {
@@ -20,6 +21,8 @@ export class RimoriClient {
20
21
  exercise;
21
22
  /** Upload and manage images stored in Supabase via the backend. */
22
23
  storage;
24
+ /** Upload assets (image/audio/video/file) backing asset-typed db.config columns. */
25
+ assets;
23
26
  /** The EventBus instance used by this client. In federation mode this is a per-plugin instance. */
24
27
  eventBus;
25
28
  constructor(controller, supabase, info, eventBus) {
@@ -35,6 +38,7 @@ export class RimoriClient {
35
38
  this.plugin = new PluginModule(supabase, controller, info, this.ai);
36
39
  this.exercise = new ExerciseModule(supabase, controller, info, this.event);
37
40
  this.storage = new StorageModule(controller);
41
+ this.assets = new AssetsModule(controller);
38
42
  //only init logger in workers and on main plugin pages
39
43
  if (this.plugin.applicationMode !== 'sidebar') {
40
44
  Logger.getInstance(this);
@@ -110,6 +110,19 @@ export declare class AIModule {
110
110
  * A warning is logged to the console in this case.
111
111
  */
112
112
  getVoice(text: string, voice?: string, speed?: number, language?: string, cache?: boolean, instructions?: string): Promise<Blob>;
113
+ /**
114
+ * Generate an image from a text prompt using AI.
115
+ * @param params.prompt The prompt describing the image to generate.
116
+ * @param params.cache Whether to cache the result by prompt hash (default: true).
117
+ * @returns `{ url, cached }` where `url` is either a data URL or a stored CDN URL.
118
+ */
119
+ getImage(params: {
120
+ prompt: string;
121
+ cache?: boolean;
122
+ }): Promise<{
123
+ url: string;
124
+ cached: boolean;
125
+ }>;
113
126
  /**
114
127
  * Convert voice audio to text using AI.
115
128
  * @param file The audio file to convert.
@@ -115,6 +115,27 @@ export class AIModule {
115
115
  }),
116
116
  }).then((r) => r.blob());
117
117
  }
118
+ /**
119
+ * Generate an image from a text prompt using AI.
120
+ * @param params.prompt The prompt describing the image to generate.
121
+ * @param params.cache Whether to cache the result by prompt hash (default: true).
122
+ * @returns `{ url, cached }` where `url` is either a data URL or a stored CDN URL.
123
+ */
124
+ async getImage(params) {
125
+ const { prompt, cache = true } = params;
126
+ const response = await this.controller.fetchBackend('/ai/image', {
127
+ method: 'POST',
128
+ body: JSON.stringify({
129
+ prompt,
130
+ cache,
131
+ session_token_id: this.sessionTokenId ?? undefined,
132
+ }),
133
+ });
134
+ if (!response.ok) {
135
+ throw new Error(`Failed to generate image: ${response.status} ${response.statusText}`);
136
+ }
137
+ return response.json();
138
+ }
118
139
  /**
119
140
  * Convert voice audio to text using AI.
120
141
  * @param file The audio file to convert.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Assets module for the `plugin-assets` Supabase bucket.
3
+ *
4
+ * Use this to upload files (image|audio|video|file) whose URL is stored in a
5
+ * matching `type: 'image' | 'audio' | 'video' | 'file'` column in db.config.ts.
6
+ *
7
+ * Lifecycle is automatic: the backend asset-refs cron links the upload to the
8
+ * row whose column value matches the returned URL (within ~30 minutes), and
9
+ * deletes the file when the row is removed or the column is replaced/cleared.
10
+ * No plugin-side confirm or delete call is needed.
11
+ *
12
+ * For markdown editor images (embedded as `![](url)` in markdown text), use
13
+ * `plugin.storage.uploadImage` instead — that has a different lifecycle (regex
14
+ * scan over markdown bodies) and lives in the separate `plugin-images` bucket.
15
+ */
16
+ import { RimoriCommunicationHandler } from '../CommunicationHandler';
17
+ export type AssetKind = 'image' | 'audio' | 'video' | 'file';
18
+ export declare class AssetsModule {
19
+ private readonly controller;
20
+ constructor(controller: RimoriCommunicationHandler);
21
+ /**
22
+ * Upload a blob as an asset of the given kind.
23
+ *
24
+ * @returns `{ data: { url, path } }` on success, `{ error }` on failure.
25
+ * Store `url` in the matching asset-typed column on your row. The cron
26
+ * will pick it up on the next tick and confirm the ref.
27
+ */
28
+ upload(blob: Blob, options: {
29
+ kind: AssetKind;
30
+ filename?: string;
31
+ }): Promise<{
32
+ data: {
33
+ url: string;
34
+ path: string;
35
+ };
36
+ error?: undefined;
37
+ } | {
38
+ data?: undefined;
39
+ error: Error;
40
+ }>;
41
+ }
@@ -0,0 +1,34 @@
1
+ export class AssetsModule {
2
+ controller;
3
+ constructor(controller) {
4
+ this.controller = controller;
5
+ }
6
+ /**
7
+ * Upload a blob as an asset of the given kind.
8
+ *
9
+ * @returns `{ data: { url, path } }` on success, `{ error }` on failure.
10
+ * Store `url` in the matching asset-typed column on your row. The cron
11
+ * will pick it up on the next tick and confirm the ref.
12
+ */
13
+ async upload(blob, options) {
14
+ const formData = new FormData();
15
+ const filename = options.filename ?? `asset.${options.kind}`;
16
+ formData.append('file', blob, filename);
17
+ formData.append('kind', options.kind);
18
+ try {
19
+ const response = await this.controller.fetchBackend('/plugin-assets/upload', {
20
+ method: 'POST',
21
+ body: formData,
22
+ });
23
+ if (!response.ok) {
24
+ const body = (await response.json().catch(() => ({})));
25
+ return { error: new Error(body.message ?? `Upload failed (${response.status})`) };
26
+ }
27
+ const result = (await response.json());
28
+ return { data: result };
29
+ }
30
+ catch (err) {
31
+ return { error: err instanceof Error ? err : new Error(String(err)) };
32
+ }
33
+ }
34
+ }
@@ -1,8 +1,10 @@
1
1
  import { ObjectTool } from '../../fromRimori/PluginTypes';
2
2
  import { SupabaseClient } from '../CommunicationHandler';
3
3
  import { RimoriClient } from '../RimoriClient';
4
+ import { LanguageLevel } from '../../utils/difficultyConverter';
4
5
  export type SharedContent<T> = BasicSharedContent & T;
5
6
  export type ContentStatus = 'featured' | 'community' | 'unverified';
7
+ export type SharedContentSkillType = 'grammar' | 'reading' | 'writing' | 'speaking' | 'listening' | 'understanding';
6
8
  export interface BasicSharedContent {
7
9
  id: string;
8
10
  title: string;
@@ -12,6 +14,7 @@ export interface BasicSharedContent {
12
14
  created_at: string;
13
15
  guild_id: string | null;
14
16
  lang_id: string | null;
17
+ skill_level: LanguageLevel;
15
18
  }
16
19
  export interface SharedContentCompletionState {
17
20
  content_id: string;
@@ -44,7 +47,7 @@ export declare class SharedContentController {
44
47
  */
45
48
  getNew<T>(params: {
46
49
  table: string;
47
- skillType: 'grammar' | 'reading' | 'writing' | 'speaking' | 'listening' | 'understanding';
50
+ skillType: SharedContentSkillType;
48
51
  placeholders?: Record<string, string>;
49
52
  filter?: Record<string, {
50
53
  filterType: 'rag' | 'exact' | 'exclude';
@@ -129,11 +132,15 @@ export declare class SharedContentController {
129
132
  getAll<T = any>(tableName: string, limit?: number): Promise<SharedContent<T>[]>;
130
133
  /**
131
134
  * Create new shared content manually.
135
+ * Auto-fills `skill_level` from the user's current `skill_level_{skillType}` setting
136
+ * so callers don't have to wire it up themselves (the column is NOT NULL on every
137
+ * shared-content table).
132
138
  * @param tableName - Name of the shared content table
139
+ * @param skillType - Skill this content trains; selects which user skill level to record
133
140
  * @param content - Content to create
134
141
  * @returns Created content
135
142
  */
136
- create<T = any>(tableName: string, content: Omit<SharedContent<T>, 'id' | 'created_at' | 'created_by'>): Promise<SharedContent<T>>;
143
+ create<T = any>(tableName: string, skillType: SharedContentSkillType, content: Omit<SharedContent<T>, 'id' | 'created_at' | 'created_by' | 'skill_level'>): Promise<SharedContent<T>>;
137
144
  /**
138
145
  * Update existing shared content via backend.
139
146
  * If content was already validated (community/featured) and user is not a moderator,
@@ -226,13 +226,23 @@ export class SharedContentController {
226
226
  }
227
227
  /**
228
228
  * Create new shared content manually.
229
+ * Auto-fills `skill_level` from the user's current `skill_level_{skillType}` setting
230
+ * so callers don't have to wire it up themselves (the column is NOT NULL on every
231
+ * shared-content table).
229
232
  * @param tableName - Name of the shared content table
233
+ * @param skillType - Skill this content trains; selects which user skill level to record
230
234
  * @param content - Content to create
231
235
  * @returns Created content
232
236
  */
233
- async create(tableName, content) {
237
+ async create(tableName, skillType, content) {
234
238
  const fullTableName = this.getTableName(tableName);
235
- const { data, error } = await this.supabase.from(fullTableName).insert(content).select().single();
239
+ const userInfo = this.rimoriClient.plugin.getUserInfo();
240
+ const skillLevel = userInfo[`skill_level_${skillType}`];
241
+ const { data, error } = await this.supabase
242
+ .from(fullTableName)
243
+ .insert({ ...content, skill_level: skillLevel })
244
+ .select()
245
+ .single();
236
246
  if (error) {
237
247
  console.error('Error creating shared content:', error);
238
248
  throw new Error('Error creating shared content');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/client",
3
- "version": "2.5.37",
3
+ "version": "2.5.38-next.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {
@@ -32,7 +32,7 @@
32
32
  "scripts": {
33
33
  "build": "tsc",
34
34
  "dev": "tsc -w --preserveWatchOutput",
35
- "lint": "npx eslint . --fix",
35
+ "lint": "pnpm exec eslint . --fix",
36
36
  "format": "prettier --write ."
37
37
  },
38
38
  "dependencies": {