@heyputer/puter.js 2.2.5 → 2.2.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyputer/puter.js",
3
- "version": "2.2.5",
3
+ "version": "2.2.8",
4
4
  "description": "Puter.js - A JavaScript library for interacting with Puter services.",
5
5
  "homepage": "https://developer.puter.com",
6
6
  "main": "src/index.js",
@@ -42,6 +42,7 @@
42
42
  "webpack-cli": "^5.1.4"
43
43
  },
44
44
  "dependencies": {
45
- "@heyputer/kv.js": "^0.2.1"
45
+ "@heyputer/kv.js": "^0.2.1",
46
+ "open": "^10.2.0"
46
47
  }
47
48
  }
package/src/index.js CHANGED
@@ -54,7 +54,7 @@ class SimpleLogger {
54
54
 
55
55
  _prefix () {
56
56
  const entries = Object.entries(this.fieldsObj);
57
- if ( !entries.length ) return [];
57
+ if ( ! entries.length ) return [];
58
58
  return [`[${ entries.map(([k, v]) => `${k}=${v}`).join(' ')}]`];
59
59
  }
60
60
  }
@@ -66,7 +66,7 @@ class Lock {
66
66
  }
67
67
 
68
68
  async acquire () {
69
- if ( !this.locked ) {
69
+ if ( ! this.locked ) {
70
70
  this.locked = true;
71
71
  return;
72
72
  }
@@ -374,7 +374,7 @@ const puterInit = (function () {
374
374
  }]`;
375
375
  logger = logger.fields({ prefix });
376
376
  this.logger = logger;
377
- } catch (error) {
377
+ } catch ( error ) {
378
378
  if ( this.debugMode ) {
379
379
  console.error('Failed to initialize prefix logger', error);
380
380
  }
@@ -536,6 +536,9 @@ const puterInit = (function () {
536
536
  };
537
537
 
538
538
  resetAuthToken = function () {
539
+ if ( this.env === 'worker' || this.env === 'service-worker' ) {
540
+ throw new Error('Sign out is not permitted from WebWorkers or ServiceWorkers');
541
+ }
539
542
  this.authToken = null;
540
543
  // If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage
541
544
  if ( this.env === 'web' || this.env === 'app' ) {
package/src/init.cjs CHANGED
@@ -1,6 +1,8 @@
1
1
  const { readFileSync } = require('node:fs');
2
2
  const vm = require('node:vm');
3
3
  const { resolve } = require('node:path');
4
+ const { IncomingMessage } = require('node:http');
5
+ const open = require('open');
4
6
  /**
5
7
  * Method for loading puter.js in Node.js environment with auth token
6
8
  * @param {string} authToken - Optional auth token to initialize puter with
@@ -28,4 +30,22 @@ const init = (authToken) => {
28
30
  return goodContext.puter;
29
31
  };
30
32
 
31
- module.exports = { init };
33
+ const getAuthToken = (guiOrigin = 'https://puter.com') => {
34
+ const http = require('http');
35
+
36
+ return new Promise((resolve) => {
37
+ const requestListener = function (/**@type {IncomingMessage} */ req, res) {
38
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
39
+ res.end('Authentication Granted! You maty now close this window.');
40
+
41
+ resolve(new URL(req.url, 'http://localhost/').searchParams.get('token'));
42
+ };
43
+ const server = http.createServer(requestListener);
44
+ server.listen(0, function () {
45
+ const url = `${guiOrigin}/?action=authme&redirectURL=${encodeURIComponent('http://localhost:') + this.address().port}`;
46
+ open.default(url);
47
+ });
48
+ });
49
+ };
50
+
51
+ module.exports = { init, getAuthToken };
package/src/modules/AI.js CHANGED
@@ -70,7 +70,7 @@ class AI {
70
70
 
71
71
  const tryFetchModels = async () => {
72
72
  const resp = await fetch(`${this.APIOrigin }/puterai/chat/models/details`, { headers });
73
- if ( !resp.ok ) return null;
73
+ if ( ! resp.ok ) return null;
74
74
  const data = await resp.json();
75
75
  const models = Array.isArray(data?.models) ? data.models : [];
76
76
  return provider ? models.filter(model => model.provider === provider) : models;
@@ -298,13 +298,13 @@ class AI {
298
298
  if ( ! options.voice ) {
299
299
  options.voice = '21m00Tcm4TlvDq8ikWAM';
300
300
  }
301
- if ( ! options.model && typeof options.engine === 'string' ) {
301
+ if ( !options.model && typeof options.engine === 'string' ) {
302
302
  options.model = options.engine;
303
303
  }
304
304
  if ( ! options.model ) {
305
305
  options.model = 'eleven_multilingual_v2';
306
306
  }
307
- if ( ! options.output_format && !options.response_format ) {
307
+ if ( !options.output_format && !options.response_format ) {
308
308
  options.output_format = 'mp3_44100_128';
309
309
  }
310
310
  if ( options.response_format && !options.output_format ) {
@@ -736,6 +736,10 @@ class AI {
736
736
  requestParams.max_tokens = userParams.max_tokens;
737
737
  }
738
738
 
739
+ if ( userParams.provider ) {
740
+ requestParams.provider = userParams.provider;
741
+ }
742
+
739
743
  // convert undefined to empty string so that .startsWith works
740
744
  requestParams.model = requestParams.model ?? '';
741
745
 
@@ -745,11 +749,11 @@ class AI {
745
749
  }
746
750
 
747
751
  if ( userParams.driver ) {
748
- driver = userParams.driver;
752
+ requestParams.provider = requestParams.provider || userParams.driver;
749
753
  }
750
754
 
751
755
  // Additional parameters to pass from userParams to requestParams
752
- const PARAMS_TO_PASS = ['tools', 'response', 'reasoning', 'reasoning_effort', 'text', 'verbosity'];
756
+ const PARAMS_TO_PASS = ['tools', 'response', 'reasoning', 'reasoning_effort', 'text', 'verbosity', 'provider'];
753
757
  for ( const name of PARAMS_TO_PASS ) {
754
758
  if ( userParams[name] ) {
755
759
  requestParams[name] = userParams[name];
@@ -835,8 +839,8 @@ class AI {
835
839
  options.model = 'gemini-2.5-flash-image-preview';
836
840
  }
837
841
 
838
- if (options.model === "nano-banana-pro") {
839
- options.model = "gemini-3-pro-image-preview";
842
+ if ( options.model === 'nano-banana-pro' ) {
843
+ options.model = 'gemini-3-pro-image-preview';
840
844
  }
841
845
 
842
846
  const driverHint = typeof options.driver === 'string' ? options.driver : undefined;
@@ -912,6 +916,7 @@ class AI {
912
916
  options.seconds = options.duration;
913
917
  }
914
918
 
919
+ // This sucks, should be backend's job like we do for chat models now
915
920
  let videoService = 'openai-video-generation';
916
921
  const driverHint = typeof options.driver === 'string' ? options.driver : undefined;
917
922
  const driverHintLower = driverHint ? driverHint.toLowerCase() : undefined;
@@ -922,7 +927,7 @@ class AI {
922
927
  const modelLower = typeof options.model === 'string' ? options.model.toLowerCase() : '';
923
928
 
924
929
  const looksLikeTogetherVideoModel = typeof options.model === 'string' &&
925
- TOGETHER_VIDEO_MODEL_PREFIXES.some(prefix => modelLower.startsWith(prefix));
930
+ (TOGETHER_VIDEO_MODEL_PREFIXES.some(prefix => modelLower.startsWith(prefix)) || options.model.startsWith('togetherai:'));
926
931
 
927
932
  if ( driverHintLower === 'together' || driverHintLower === 'together-ai' ) {
928
933
  videoService = 'together-video-generation';
@@ -958,7 +963,8 @@ class AI {
958
963
  return result;
959
964
  }
960
965
 
961
- const video = (globalThis.document?.createElement('video') || {setAttribute: ()=>{}});
966
+ const video = (globalThis.document?.createElement('video') || { setAttribute: () => {
967
+ } });
962
968
  video.src = sourceUrl;
963
969
  video.controls = true;
964
970
  video.preload = 'metadata';
@@ -217,7 +217,7 @@ class Apps {
217
217
  };
218
218
  }
219
219
 
220
- return new Promise((resolve, reject) => {
220
+ return new Promise((resUpper, rejUpper) => {
221
221
  let options;
222
222
 
223
223
  // If first argument is an object, it's the options
@@ -235,7 +235,7 @@ class Apps {
235
235
  const xhr = utils.initXhr('/get-dev-profile', puter.APIOrigin, puter.authToken, 'get');
236
236
 
237
237
  // set up event handlers for load and error events
238
- utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
238
+ utils.setupXhrEventHandlers(xhr, options.success ?? resUpper, options.error ?? rejUpper, resolve, reject);
239
239
 
240
240
  xhr.send();
241
241
  });
@@ -2,6 +2,97 @@ import path from '../../../lib/path.js';
2
2
  import * as utils from '../../../lib/utils.js';
3
3
  import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
4
4
 
5
+ const MAX_THUMBNAIL_BYTES = 2 * 1024 * 1024;
6
+ const DEFAULT_THUMBNAIL_DIMENSION = 128;
7
+ const MIN_THUMBNAIL_DIMENSION = 32;
8
+
9
+ const isLikelyImageFile = (file) => {
10
+ if ( ! file ) return false;
11
+ if ( file.type && file.type.startsWith('image/') ) return true;
12
+ const name = (file.name || '').toLowerCase();
13
+ return ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff', '.avif', '.jfif'].some(ext => name.endsWith(ext));
14
+ };
15
+
16
+ const estimateDataUrlSize = (dataUrl) => {
17
+ if ( ! dataUrl ) return 0;
18
+ const commaIndex = dataUrl.indexOf(',');
19
+ const base64 = commaIndex === -1 ? dataUrl : dataUrl.slice(commaIndex + 1);
20
+ return Math.ceil(base64.length * 3 / 4);
21
+ };
22
+
23
+ const scaleDimensions = (width, height, maxDim) => {
24
+ const base = Math.max(width, height) || 1;
25
+ const scale = Math.min(1, maxDim / base);
26
+ const w = Math.max(1, Math.round(width * scale));
27
+ const h = Math.max(1, Math.round(height * scale));
28
+ return { width: w, height: h };
29
+ };
30
+
31
+ const loadImageFromFile = (file) => new Promise((resolve, reject) => {
32
+ if ( typeof document === 'undefined' || typeof URL === 'undefined' || typeof Image === 'undefined' ) return resolve(null);
33
+ const url = URL.createObjectURL(file);
34
+ const img = new Image();
35
+ img.onload = () => {
36
+ URL.revokeObjectURL(url);
37
+ resolve(img);
38
+ };
39
+ img.onerror = (e) => {
40
+ URL.revokeObjectURL(url);
41
+ reject(e);
42
+ };
43
+ img.src = url;
44
+ });
45
+
46
+ const renderThumbnail = (img, maxDim, type, quality) => {
47
+ if ( !img || typeof document === 'undefined' ) return null;
48
+ const { width, height } = scaleDimensions(img.naturalWidth || img.width, img.naturalHeight || img.height, maxDim);
49
+ const canvas = document.createElement('canvas');
50
+ canvas.width = width;
51
+ canvas.height = height;
52
+ const ctx = canvas.getContext('2d');
53
+ if ( ! ctx ) return null;
54
+ ctx.drawImage(img, 0, 0, width, height);
55
+ try {
56
+ return canvas.toDataURL(type, quality);
57
+ } catch (e) {
58
+ return null;
59
+ }
60
+ };
61
+
62
+ const defaultThumbnailGenerator = async (file) => {
63
+ try {
64
+ if ( typeof document === 'undefined' ) return undefined;
65
+ if ( typeof File === 'undefined' || !(file instanceof File) ) return undefined;
66
+ if ( ! isLikelyImageFile(file) ) return undefined;
67
+
68
+ const img = await loadImageFromFile(file);
69
+ if ( ! img ) return undefined;
70
+
71
+ let dimension = DEFAULT_THUMBNAIL_DIMENSION;
72
+ const formats = [
73
+ { type: 'image/webp', quality: 0.85 },
74
+ { type: 'image/jpeg', quality: 0.8 },
75
+ { type: 'image/png' },
76
+ ];
77
+
78
+ while ( dimension >= MIN_THUMBNAIL_DIMENSION ) {
79
+ for ( const { type, quality } of formats ) {
80
+ const dataUrl = renderThumbnail(img, dimension, type, quality);
81
+ if ( ! dataUrl ) continue;
82
+ if ( estimateDataUrlSize(dataUrl) <= MAX_THUMBNAIL_BYTES ) {
83
+ return dataUrl;
84
+ }
85
+ }
86
+ dimension = Math.floor(dimension / 2);
87
+ }
88
+ } catch (e) {
89
+ // Ignore thumbnail errors; upload should proceed without them.
90
+ return undefined;
91
+ }
92
+
93
+ return undefined;
94
+ };
95
+
5
96
  /* eslint-disable */
6
97
  const upload = async function (items, dirPath, options = {}) {
7
98
  return new Promise(async (resolve, reject) => {
@@ -200,6 +291,19 @@ const upload = async function (items, dirPath, options = {}) {
200
291
  return error({ code: 'EMPTY_UPLOAD', message: 'No files or directories to upload.' });
201
292
  }
202
293
 
294
+ let thumbnails = [];
295
+ const shouldGenerateThumbnails = options.generateThumbnails || options.thumbnailGenerator;
296
+ if ( files.length && shouldGenerateThumbnails ) {
297
+ const generator = options.thumbnailGenerator || defaultThumbnailGenerator;
298
+ thumbnails = await Promise.all(files.map(async (file) => {
299
+ try {
300
+ return await generator(file);
301
+ } catch (e) {
302
+ return undefined;
303
+ }
304
+ }));
305
+ }
306
+
203
307
  // Check storage capacity.
204
308
  // We need to check the storage capacity before the upload starts because
205
309
  // we want to avoid uploading files in case there is not enough storage space.
@@ -287,20 +391,28 @@ const upload = async function (items, dirPath, options = {}) {
287
391
  // Append file metadata to upload request
288
392
  if ( ! options.shortcutTo ) {
289
393
  for ( let i = 0; i < files.length; i++ ) {
290
- fd.append('fileinfo', JSON.stringify({
394
+ const thumbnail = thumbnails[i] ?? options.thumbnail ?? undefined;
395
+ const fileinfo_payload = {
291
396
  name: files[i].name,
292
397
  type: files[i].type,
293
398
  size: files[i].size,
399
+ };
400
+ if ( thumbnail ) {
401
+ fileinfo_payload.thumbnail = thumbnail;
402
+ }
403
+ fd.append('fileinfo', JSON.stringify({
404
+ ...fileinfo_payload,
294
405
  }));
295
406
  }
296
407
  }
297
408
  // Append write operations for each file
298
409
  for ( let i = 0; i < files.length; i++ ) {
299
- fd.append('operation', JSON.stringify({
410
+ const thumbnail = thumbnails[i] ?? options.thumbnail ?? undefined;
411
+ const operation = {
300
412
  op: options.shortcutTo ? 'shortcut' : 'write',
301
413
  dedupe_name: options.dedupeName ?? true,
302
414
  overwrite: options.overwrite ?? false,
303
- thumbnail: options.thumbnail ?? undefined,
415
+ thumbnail,
304
416
  create_missing_ancestors: (options.createMissingAncestors || options.createMissingParents),
305
417
  operation_id: operation_id,
306
418
  path: (
@@ -315,7 +427,13 @@ const upload = async function (items, dirPath, options = {}) {
315
427
  shortcut_to: options.shortcutTo,
316
428
  shortcut_to_uid: options.shortcutTo,
317
429
  app_uid: options.appUID,
318
- }));
430
+ };
431
+
432
+ if ( thumbnail === undefined ) {
433
+ delete operation.thumbnail;
434
+ }
435
+
436
+ fd.append('operation', JSON.stringify(operation));
319
437
  }
320
438
 
321
439
  // Append files to upload
@@ -465,4 +583,4 @@ const upload = async function (items, dirPath, options = {}) {
465
583
  });
466
584
  };
467
585
 
468
- export default upload;
586
+ export default upload;
@@ -1,12 +1,18 @@
1
1
  import path from '../../../lib/path.js';
2
2
 
3
3
  const getAbsolutePathForApp = (relativePath) => {
4
- // if we are in the gui environment, return the relative path as is
5
- if ( puter.env === 'gui' )
4
+ // preserve previous behavior for falsy values when env is gui
5
+ if ( puter.env === 'gui' && !relativePath )
6
6
  {
7
7
  return relativePath;
8
8
  }
9
9
 
10
+ const reLooksLikeUUID = /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
11
+ const isUUID = reLooksLikeUUID.test(relativePath);
12
+ if ( isUUID ) {
13
+ return relativePath;
14
+ }
15
+
10
16
  // if no relative path is provided, use the current working directory
11
17
  if ( ! relativePath )
12
18
  {
@@ -15,8 +21,12 @@ const getAbsolutePathForApp = (relativePath) => {
15
21
 
16
22
  // If relativePath is not provided, or it's not starting with a slash or tilde,
17
23
  // it means it's a relative path. In that case, prepend the app's root directory.
18
- if ( !relativePath || (!relativePath.startsWith('/') && !relativePath.startsWith('~') && puter.appID) ) {
19
- relativePath = path.join('~/AppData', puter.appID, relativePath);
24
+ if ( !relativePath || (!relativePath.startsWith('/') && !relativePath.startsWith('~')) ) {
25
+ if ( puter.appID ) {
26
+ relativePath = path.join('~/AppData', puter.appID, relativePath);
27
+ } else {
28
+ relativePath = path.join('~/', relativePath);
29
+ }
20
30
  }
21
31
 
22
32
  return relativePath;
package/src/modules/KV.js CHANGED
@@ -350,43 +350,43 @@ class KV {
350
350
  let pattern;
351
351
  let returnValues = false;
352
352
 
353
- // list(true) or list(pattern, true) will return the key-value pairs
354
- if ( (args && args.length === 1 && args[0] === true) || (args && args.length === 2 && args[1] === true) ) {
355
- options = {};
356
- returnValues = true;
357
- }
358
- // return only the keys, default behavior
359
- else {
360
- options = { as: 'keys' };
353
+ const isOptionsObject = args.length === 1 && args[0] && typeof args[0] === 'object' && !Array.isArray(args[0]);
354
+
355
+ if ( isOptionsObject ) {
356
+ const input = args[0];
357
+ if ( typeof input.pattern === 'string' ) {
358
+ pattern = input.pattern;
359
+ }
360
+ returnValues = !!input.returnValues;
361
+ if ( input.limit !== undefined ) {
362
+ options.limit = input.limit;
363
+ }
364
+ if ( input.cursor !== undefined ) {
365
+ options.cursor = input.cursor;
366
+ }
367
+ } else {
368
+ // list(true) or list(pattern, true) will return the key-value pairs
369
+ if ( (args && args.length === 1 && args[0] === true) || (args && args.length === 2 && args[1] === true) ) {
370
+ returnValues = true;
371
+ }
372
+
373
+ // list(pattern)
374
+ // list(pattern, true)
375
+ if ( (args && args.length === 1 && typeof args[0] === 'string') || (args && args.length === 2 && typeof args[0] === 'string' && args[1] === true) ) {
376
+ pattern = args[0];
377
+ }
361
378
  }
362
379
 
363
- // list(pattern)
364
- // list(pattern, true)
365
- if ( (args && args.length === 1 && typeof args[0] === 'string') || (args && args.length === 2 && typeof args[0] === 'string' && args[1] === true) ) {
366
- pattern = args[0];
380
+ if ( ! returnValues ) {
381
+ options.as = 'keys';
367
382
  }
368
383
 
369
- return utils.make_driver_method([], 'puter-kvstore', undefined, 'list', {
370
- transform: (res) => {
371
- // glob pattern was provided
372
- if ( pattern ) {
373
- // consider both the key and the value
374
- if ( ! returnValues ) {
375
- let keys = res.filter((key) => {
376
- return globMatch(pattern, key);
377
- });
378
- return keys;
379
- } else {
380
- let keys = res.filter((key_value_pair) => {
381
- return globMatch(pattern, key_value_pair.key);
382
- });
383
- return keys;
384
- }
385
- }
384
+ const normalizedPattern = normalizeListPattern(pattern);
385
+ if ( normalizedPattern ) {
386
+ options.pattern = normalizedPattern;
387
+ }
386
388
 
387
- return res;
388
- },
389
- }).call(this, options);
389
+ return utils.make_driver_method([], 'puter-kvstore', undefined, 'list').call(this, options);
390
390
  };
391
391
 
392
392
  // resolve to 'true' on success, or rejects with an error on failure
@@ -397,18 +397,19 @@ class KV {
397
397
  clear = this.flush;
398
398
  }
399
399
 
400
- function globMatch (pattern, str) {
401
- const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
402
-
403
- let regexPattern = escapeRegExp(pattern)
404
- .replace(/\\\*/g, '.*') // Replace * with .*
405
- .replace(/\\\?/g, '.') // Replace ? with .
406
- .replace(/\\\[/g, '[') // Replace [ with [
407
- .replace(/\\\]/g, ']') // Replace ] with ]
408
- .replace(/\\\^/g, '^'); // Replace ^ with ^
409
-
410
- let re = new RegExp(`^${regexPattern}$`);
411
- return re.test(str);
400
+ function normalizeListPattern (pattern) {
401
+ if ( typeof pattern !== 'string' ) {
402
+ return undefined;
403
+ }
404
+ const trimmed = pattern.trim();
405
+ if ( trimmed === '' ) {
406
+ return undefined;
407
+ }
408
+ if ( trimmed.endsWith('*') ) {
409
+ const prefix = trimmed.slice(0, -1);
410
+ return prefix === '' ? undefined : prefix;
411
+ }
412
+ return trimmed;
412
413
  }
413
414
 
414
415
  export default KV;
@@ -19,6 +19,22 @@ export interface KVAddPath {
19
19
  [path: string]: KVValue | KVValue[];
20
20
  }
21
21
 
22
+ export interface KVListOptions {
23
+ pattern?: string;
24
+ returnValues?: boolean;
25
+ limit?: number;
26
+ cursor?: string;
27
+ }
28
+
29
+ export type KVListPaginationOptions =
30
+ | { limit: number; cursor?: string }
31
+ | { cursor: string; limit?: number };
32
+
33
+ export interface KVListPage<T = unknown> {
34
+ items: T[];
35
+ cursor?: string;
36
+ }
37
+
22
38
  export class KV {
23
39
  readonly MAX_KEY_SIZE: number;
24
40
  readonly MAX_VALUE_SIZE: number;
@@ -36,6 +52,10 @@ export class KV {
36
52
  list (pattern?: string, returnValues?: false): Promise<string[]>;
37
53
  list<T = unknown>(pattern: string, returnValues: true): Promise<KVPair<T>[]>;
38
54
  list<T = unknown>(returnValues: true): Promise<KVPair<T>[]>;
55
+ list (options: KVListOptions & KVListPaginationOptions & { returnValues?: false }): Promise<KVListPage<string>>;
56
+ list<T = unknown>(options: KVListOptions & KVListPaginationOptions & { returnValues: true }): Promise<KVListPage<KVPair<T>>>;
57
+ list (options: KVListOptions & { returnValues?: false }): Promise<string[]>;
58
+ list<T = unknown>(options: KVListOptions & { returnValues: true }): Promise<KVPair<T>[]>;
39
59
  flush (): Promise<boolean>;
40
60
  clear (): Promise<boolean>;
41
61
  }