mindsim 0.1.6 → 0.1.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/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import axios, { type AxiosInstance } from "axios";
2
- import { getApiBaseUrl, loadApiKey } from "./config";
2
+ import { getApiBaseUrl, loadApiKey, loadServiceJson, validateServiceJson } from "./config";
3
3
  import { ArtifactsResource } from "./resources/artifacts";
4
4
  import { MindTopicsResource } from "./resources/mind-topics";
5
5
  import { MindsResource } from "./resources/minds";
@@ -7,10 +7,60 @@ import { PsychometricsResource } from "./resources/psychometrics";
7
7
  import { SimulationsResource } from "./resources/simulations";
8
8
  import { SnapshotsResource } from "./resources/snapshots";
9
9
  import { TagsResource } from "./resources/tags";
10
+ import type { MindsimServiceJson } from "./types";
10
11
  import { checkForUpdates, getPackageVersion } from "./version";
11
12
 
13
+ // =============================================================================
14
+ // Gap 15 Fix: Browser Compatibility Check
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Check if the SDK is running in a Node.js environment.
19
+ * Some features (like file-based service JSON loading) only work in Node.js.
20
+ */
21
+ export function isNodeEnvironment(): boolean {
22
+ return (
23
+ typeof process !== "undefined" && process.versions != null && process.versions.node != null
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Check if the SDK is running in a browser environment.
29
+ */
30
+ export function isBrowserEnvironment(): boolean {
31
+ // Use typeof checks to avoid ReferenceError in Node.js
32
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
33
+ return typeof globalThis !== "undefined" && "window" in globalThis && "document" in globalThis;
34
+ }
35
+
36
+ // =============================================================================
37
+ // Gap 14 Fix: Auth Method Detection
38
+ // =============================================================================
39
+
40
+ /**
41
+ * Authentication method used by the MindSim client.
42
+ */
43
+ export type AuthMethod =
44
+ | "service_json" // Service JSON credentials (file or base64)
45
+ | "api_key_env" // MINDSIM_API_KEY environment variable
46
+ | "api_key_constructor" // API key passed to constructor
47
+ | "config_file"; // ~/.mindsim/config file
48
+
49
+ /**
50
+ * Options for initializing the MindSim client.
51
+ */
52
+ export interface MindSimOptions {
53
+ /** Custom API base URL (overrides default and service JSON) */
54
+ apiBaseUrl?: string;
55
+ /** Service JSON credentials (alternative to API key) */
56
+ serviceJson?: MindsimServiceJson;
57
+ }
58
+
12
59
  export class MindSim {
13
60
  private client: AxiosInstance;
61
+ private serviceJson: MindsimServiceJson | null = null;
62
+ // Gap 14 Fix: Track which auth method was used
63
+ private authMethod: AuthMethod = "config_file";
14
64
 
15
65
  public artifacts: ArtifactsResource;
16
66
  public minds: MindsResource;
@@ -20,20 +70,72 @@ export class MindSim {
20
70
  public simulations: SimulationsResource;
21
71
  public tags: TagsResource;
22
72
 
23
- constructor(apiKey?: string, options?: { apiBaseUrl?: string }) {
73
+ constructor(apiKey?: string, options?: MindSimOptions) {
24
74
  // 1. Trigger the auto-update check (Fire and forget, do not await)
25
75
  checkForUpdates().catch(() => {
26
76
  // Catching here ensures no unhandled promise rejections in the user's console
27
77
  // if the check fails completely.
28
78
  });
29
79
 
30
- const token = apiKey || loadApiKey();
31
80
  const sdkVersion = getPackageVersion();
32
- const apiBaseUrl = options?.apiBaseUrl || getApiBaseUrl();
81
+
82
+ // Priority for credentials:
83
+ // 1. Constructor apiKey parameter
84
+ // 2. Constructor serviceJson option
85
+ // 3. Environment service JSON (MINDSIM_SERVICE_JSON or MINDSIM_SERVICE_JSON_BASE64)
86
+ // 4. Environment API key (MINDSIM_API_KEY)
87
+ // 5. Config file (~/.mindsim/config)
88
+
89
+ let token: string | null = apiKey || null;
90
+ let apiBaseUrl = options?.apiBaseUrl || getApiBaseUrl();
91
+
92
+ // Track auth method for Gap 14 fix
93
+ if (apiKey) {
94
+ this.authMethod = "api_key_constructor";
95
+ }
96
+
97
+ // Check for service JSON if no direct API key provided
98
+ if (!token) {
99
+ let serviceJson: MindsimServiceJson | null = null;
100
+
101
+ // Gap 3 Fix: Validate serviceJson when passed via constructor options
102
+ if (options?.serviceJson) {
103
+ const validation = validateServiceJson(options.serviceJson);
104
+ if (!validation.valid) {
105
+ throw new Error(`Invalid serviceJson: ${validation.errors.join(", ")}`);
106
+ }
107
+ serviceJson = validation.serviceJson as MindsimServiceJson;
108
+ } else {
109
+ // loadServiceJson() already validates internally
110
+ serviceJson = loadServiceJson();
111
+ }
112
+
113
+ if (serviceJson) {
114
+ this.serviceJson = serviceJson;
115
+ this.authMethod = "service_json";
116
+ token = serviceJson.api_key;
117
+ // Use API base URL from service JSON if not explicitly overridden
118
+ if (!options?.apiBaseUrl && serviceJson.api_base_url) {
119
+ apiBaseUrl = serviceJson.api_base_url;
120
+ }
121
+ }
122
+ }
123
+
124
+ // Fallback to traditional loading
125
+ if (!token) {
126
+ // Gap 7 Fix: Check if MINDSIM_API_KEY is set (only in Node environment)
127
+ const hasEnvApiKey = isNodeEnvironment() && process.env.MINDSIM_API_KEY;
128
+ if (hasEnvApiKey) {
129
+ this.authMethod = "api_key_env";
130
+ } else {
131
+ this.authMethod = "config_file";
132
+ }
133
+ token = loadApiKey();
134
+ }
33
135
 
34
136
  if (!token) {
35
137
  throw new Error(
36
- "API Key not found. Please run `mindsim auth` or pass the key to the constructor.",
138
+ "API Key not found. Please run `mindsim auth`, set MINDSIM_SERVICE_JSON/MINDSIM_SERVICE_JSON_BASE64, or pass credentials to the constructor.",
37
139
  );
38
140
  }
39
141
 
@@ -45,6 +147,10 @@ export class MindSim {
45
147
  "x-reasoner-source": "sdk",
46
148
  "x-reasoner-sdk": "mindsim/mindsim-sdk-typescript",
47
149
  "x-reasoner-sdk-version": sdkVersion,
150
+ // Include project ID from service JSON for better request tracing
151
+ ...(this.serviceJson && {
152
+ "x-mindsim-project-id": this.serviceJson.project_id,
153
+ }),
48
154
  },
49
155
  });
50
156
 
@@ -56,7 +162,73 @@ export class MindSim {
56
162
  this.simulations = new SimulationsResource(this.client);
57
163
  this.tags = new TagsResource(this.client);
58
164
  }
165
+
166
+ /**
167
+ * Get the loaded service JSON (if any).
168
+ * Useful for debugging or accessing project metadata.
169
+ *
170
+ * @returns The service JSON object or null if not using service JSON auth
171
+ */
172
+ public getServiceJson(): MindsimServiceJson | null {
173
+ return this.serviceJson;
174
+ }
175
+
176
+ // ==========================================================================
177
+ // Gap 14 Fix: Auth Method Detection Helpers
178
+ // ==========================================================================
179
+
180
+ /**
181
+ * Check if the client is using service JSON authentication.
182
+ *
183
+ * @returns true if using service JSON, false otherwise
184
+ */
185
+ public isUsingServiceJsonAuth(): boolean {
186
+ return this.serviceJson !== null;
187
+ }
188
+
189
+ /**
190
+ * Get the authentication method used by this client.
191
+ *
192
+ * @returns The auth method: 'service_json', 'api_key_env', 'api_key_constructor', or 'config_file'
193
+ */
194
+ public getAuthMethod(): AuthMethod {
195
+ return this.authMethod;
196
+ }
197
+
198
+ /**
199
+ * Get authentication info for debugging.
200
+ * Does NOT expose the actual API key.
201
+ *
202
+ * @returns Object with auth details (method, project info if service JSON)
203
+ */
204
+ public getAuthInfo(): {
205
+ method: AuthMethod;
206
+ isServiceJson: boolean;
207
+ projectId: string | null;
208
+ projectName: string | null;
209
+ environment: string | null;
210
+ } {
211
+ return {
212
+ method: this.authMethod,
213
+ isServiceJson: this.serviceJson !== null,
214
+ projectId: this.serviceJson?.project_id ?? null,
215
+ projectName: this.serviceJson?.project_name ?? null,
216
+ environment: this.serviceJson?.environment ?? null,
217
+ };
218
+ }
59
219
  }
60
220
 
221
+ // Export validation utilities from config
222
+ export { type ServiceJsonValidationResult, validateServiceJson } from "./config";
223
+
224
+ // Export logger interface (Gap 8 Fix)
225
+ export {
226
+ createSilentLogger,
227
+ createVerboseLogger,
228
+ defaultLogger,
229
+ getLogger,
230
+ type MindSimLogger,
231
+ setLogger,
232
+ } from "./logger";
61
233
  // Export types for consumer use
62
234
  export * from "./types";
package/src/logger.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * MindSim SDK Logger Interface
3
+ *
4
+ * Provides a configurable logging interface so users can control
5
+ * how SDK messages are logged (or suppressed).
6
+ *
7
+ * Gap 8 Fix: Replaces hardcoded console.error with configurable interface.
8
+ *
9
+ * Usage:
10
+ * import { setLogger, createSilentLogger } from 'mindsim';
11
+ *
12
+ * // Use silent logger to suppress all SDK logs
13
+ * setLogger(createSilentLogger());
14
+ *
15
+ * // Use custom logger
16
+ * setLogger({
17
+ * debug: (msg) => myLogger.debug(`[MindSim] ${msg}`),
18
+ * info: (msg) => myLogger.info(`[MindSim] ${msg}`),
19
+ * warn: (msg) => myLogger.warn(`[MindSim] ${msg}`),
20
+ * error: (msg) => myLogger.error(`[MindSim] ${msg}`),
21
+ * });
22
+ */
23
+
24
+ /**
25
+ * Logger interface for the MindSim SDK.
26
+ * Implement this interface to customize SDK logging behavior.
27
+ */
28
+ export interface MindSimLogger {
29
+ /** Debug-level messages (not shown by default) */
30
+ debug: (message: string, ...args: unknown[]) => void;
31
+ /** Informational messages */
32
+ info: (message: string, ...args: unknown[]) => void;
33
+ /** Warning messages */
34
+ warn: (message: string, ...args: unknown[]) => void;
35
+ /** Error messages */
36
+ error: (message: string, ...args: unknown[]) => void;
37
+ }
38
+
39
+ /**
40
+ * Default logger that uses console methods.
41
+ * Debug is disabled by default.
42
+ */
43
+ export const defaultLogger: MindSimLogger = {
44
+ debug: () => {
45
+ // Debug disabled by default
46
+ },
47
+ info: (message: string, ...args: unknown[]) => {
48
+ console.info(`[MindSim] ${message}`, ...args);
49
+ },
50
+ warn: (message: string, ...args: unknown[]) => {
51
+ console.warn(`[MindSim] ${message}`, ...args);
52
+ },
53
+ error: (message: string, ...args: unknown[]) => {
54
+ console.error(`[MindSim] ${message}`, ...args);
55
+ },
56
+ };
57
+
58
+ /**
59
+ * Create a silent logger that suppresses all SDK logs.
60
+ * Useful for production environments where you want no SDK output.
61
+ */
62
+ export function createSilentLogger(): MindSimLogger {
63
+ const noop = () => {};
64
+ return {
65
+ debug: noop,
66
+ info: noop,
67
+ warn: noop,
68
+ error: noop,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Create a logger with all levels enabled (including debug).
74
+ * Useful for development and debugging.
75
+ */
76
+ export function createVerboseLogger(): MindSimLogger {
77
+ return {
78
+ debug: (message: string, ...args: unknown[]) => {
79
+ console.debug(`[MindSim:DEBUG] ${message}`, ...args);
80
+ },
81
+ info: (message: string, ...args: unknown[]) => {
82
+ console.info(`[MindSim] ${message}`, ...args);
83
+ },
84
+ warn: (message: string, ...args: unknown[]) => {
85
+ console.warn(`[MindSim] ${message}`, ...args);
86
+ },
87
+ error: (message: string, ...args: unknown[]) => {
88
+ console.error(`[MindSim] ${message}`, ...args);
89
+ },
90
+ };
91
+ }
92
+
93
+ // Global logger instance
94
+ let currentLogger: MindSimLogger = defaultLogger;
95
+
96
+ /**
97
+ * Set the global logger for the SDK.
98
+ *
99
+ * @param logger - The logger implementation to use
100
+ */
101
+ export function setLogger(logger: MindSimLogger): void {
102
+ currentLogger = logger;
103
+ }
104
+
105
+ /**
106
+ * Get the current logger.
107
+ * Used internally by the SDK.
108
+ */
109
+ export function getLogger(): MindSimLogger {
110
+ return currentLogger;
111
+ }
package/src/types.ts CHANGED
@@ -55,6 +55,65 @@ export interface SdkKeyDetailResponse {
55
55
  };
56
56
  }
57
57
 
58
+ /**
59
+ * Valid MindSim scopes for API access.
60
+ * Matches rainmaker's MindsimScope type (lib/types/developer-portal.ts).
61
+ */
62
+ export type MindsimScope =
63
+ | "minds:read" // List/read digital twins
64
+ | "minds:write" // Create/update digital twins
65
+ | "simulate:run" // Run simulations
66
+ | "simulate:read" // Read simulation history
67
+ | "users:read" // Read org users
68
+ | "users:write" // Manage org users
69
+ | "org:admin"; // Full org administration
70
+
71
+ /**
72
+ * Valid use cases for developer apps.
73
+ * Matches rainmaker's DeveloperUseCase type.
74
+ */
75
+ export type DeveloperUseCase = "internal-workflow" | "customer-facing" | "agentic-ai";
76
+
77
+ /**
78
+ * MindSim Service JSON credential format.
79
+ * Google Cloud-style service account JSON for production deployments.
80
+ */
81
+ export interface MindsimServiceJson {
82
+ /** Must be "mindsim_service_account" to identify this as a service JSON */
83
+ type: "mindsim_service_account";
84
+ /** Service JSON schema version */
85
+ version: string;
86
+ /** App slug (human-readable identifier) */
87
+ project_id: string;
88
+ /** App display name */
89
+ project_name: string;
90
+ /** Environment (always "production" for service JSON) */
91
+ environment: "production";
92
+ /** Service account email (service@{slug}.mindsim.io) */
93
+ client_email: string;
94
+ /** API key UUID */
95
+ client_id: string;
96
+ /** The actual API key secret */
97
+ api_key: string;
98
+ /** API endpoint base URL */
99
+ api_base_url: string;
100
+ /** Granted API scopes */
101
+ scopes: MindsimScope[];
102
+ /** Application metadata */
103
+ app_metadata: {
104
+ app_id: string;
105
+ workspace_id: string;
106
+ mindsim_org_id: string;
107
+ use_case: DeveloperUseCase;
108
+ };
109
+ /** ISO 8601 timestamp when credentials were created */
110
+ created_at: string;
111
+ /** Optional: ISO 8601 timestamp when credentials expire */
112
+ expires_at?: string;
113
+ /** Optional: ISO 8601 timestamp when credentials were issued (same as created_at for new keys) */
114
+ issued_at?: string;
115
+ }
116
+
58
117
  export interface Tag {
59
118
  id: string;
60
119
  name: string;
package/src/version.ts CHANGED
@@ -4,39 +4,96 @@ import path from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import axios from "axios";
6
6
  import semver from "semver";
7
+ import { getLogger } from "./logger";
7
8
 
8
9
  const execAsync = promisify(exec);
9
10
 
11
+ type PackageManager = "pnpm" | "yarn" | "npm";
12
+
13
+ interface InstallContext {
14
+ isGlobal: boolean;
15
+ packageManager: PackageManager;
16
+ }
17
+
10
18
  /**
11
- * Detects the package manager used in the current project
19
+ * Detects the installation context (global vs local, and which package manager)
12
20
  */
13
- const detectPackageManager = (): "pnpm" | "yarn" | "npm" => {
14
- const cwd = process.cwd();
15
-
16
- // Check for lockfiles in order of preference
17
- if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
18
- return "pnpm";
21
+ const detectInstallContext = (): InstallContext => {
22
+ const home = process.env.HOME || "";
23
+ const appData = process.env.APPDATA || "";
24
+
25
+ // Global path patterns mapped to their package managers
26
+ const globalPatterns: Array<{ pattern: string; pm: PackageManager }> = [
27
+ // pnpm global paths
28
+ { pattern: path.join(home, "Library/pnpm"), pm: "pnpm" },
29
+ { pattern: path.join(home, ".local/share/pnpm"), pm: "pnpm" },
30
+ { pattern: path.join(appData, "pnpm"), pm: "pnpm" },
31
+ // yarn global paths
32
+ { pattern: path.join(home, ".yarn"), pm: "yarn" },
33
+ { pattern: path.join(home, ".config/yarn"), pm: "yarn" },
34
+ { pattern: path.join(appData, "Yarn"), pm: "yarn" },
35
+ // npm global paths (including nvm)
36
+ { pattern: "/usr/local/lib/node_modules", pm: "npm" },
37
+ { pattern: "/usr/lib/node_modules", pm: "npm" },
38
+ { pattern: path.join(home, ".nvm"), pm: "npm" },
39
+ { pattern: path.join(home, ".npm-global"), pm: "npm" },
40
+ { pattern: path.join(appData, "npm"), pm: "npm" },
41
+ ];
42
+
43
+ // Check if we're in a global installation path
44
+ for (const { pattern, pm } of globalPatterns) {
45
+ if (pattern && __dirname.startsWith(pattern)) {
46
+ return { isGlobal: true, packageManager: pm };
47
+ }
19
48
  }
20
- if (fs.existsSync(path.join(cwd, "yarn.lock"))) {
21
- return "yarn";
49
+
50
+ // Check if we're in a local project's node_modules
51
+ if (__dirname.includes("/node_modules/mindsim/")) {
52
+ // Detect local package manager from lockfiles
53
+ const cwd = process.cwd();
54
+ let pm: PackageManager = "npm";
55
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
56
+ pm = "pnpm";
57
+ } else if (fs.existsSync(path.join(cwd, "yarn.lock"))) {
58
+ pm = "yarn";
59
+ }
60
+ return { isGlobal: false, packageManager: pm };
22
61
  }
23
- return "npm";
62
+
63
+ // Default: assume global npm (safest default for CLI tools)
64
+ return { isGlobal: true, packageManager: "npm" };
24
65
  };
25
66
 
26
67
  /**
27
- * Gets the install command for a specific package manager
68
+ * Gets the update command for a specific package manager.
69
+ * Uses install/add with @latest to ensure we get the absolute latest version,
70
+ * not constrained by semver ranges in package.json.
28
71
  */
29
- const getInstallCommand = (
30
- packageManager: "pnpm" | "yarn" | "npm",
72
+ const getUpdateCommand = (
73
+ packageManager: PackageManager,
31
74
  packageName: string,
75
+ global: boolean,
32
76
  ): string => {
77
+ if (global) {
78
+ switch (packageManager) {
79
+ case "pnpm":
80
+ // pnpm add -g with @latest ensures latest version
81
+ return `pnpm add -g ${packageName}@latest`;
82
+ case "yarn":
83
+ // yarn global add with @latest ensures latest version
84
+ return `yarn global add ${packageName}@latest`;
85
+ case "npm":
86
+ // npm install -g with @latest is more reliable than npm update -g
87
+ return `npm install -g ${packageName}@latest`;
88
+ }
89
+ }
33
90
  switch (packageManager) {
34
91
  case "pnpm":
35
92
  return `pnpm add ${packageName}@latest`;
36
93
  case "yarn":
37
94
  return `yarn add ${packageName}@latest`;
38
95
  case "npm":
39
- return `npm install ${packageName}@latest --save`;
96
+ return `npm install ${packageName}@latest`;
40
97
  }
41
98
  };
42
99
 
@@ -65,7 +122,7 @@ export const getPackageVersion = (): string => {
65
122
  const pkg = JSON.parse(content);
66
123
  return pkg.version;
67
124
  } catch (error) {
68
- console.warn("MindSim SDK: Unable to determine current package version.", error);
125
+ getLogger().warn("Unable to determine current package version.", error);
69
126
  return "0.0.0";
70
127
  }
71
128
  };
@@ -115,7 +172,7 @@ export const checkForUpdates = async (verbose = false): Promise<void> => {
115
172
  export const updateSdk = async (): Promise<void> => {
116
173
  console.log(`Checking for updates for ${PACKAGE_NAME}...`);
117
174
 
118
- const packageManager = detectPackageManager();
175
+ const { isGlobal, packageManager } = detectInstallContext();
119
176
 
120
177
  try {
121
178
  const currentVersion = getPackageVersion();
@@ -127,17 +184,18 @@ export const updateSdk = async (): Promise<void> => {
127
184
  return;
128
185
  }
129
186
 
130
- const installCommand = getInstallCommand(packageManager, PACKAGE_NAME);
187
+ const updateCommand = getUpdateCommand(packageManager, PACKAGE_NAME, isGlobal);
131
188
 
132
189
  console.log(`Updating from ${currentVersion} to ${latestVersion}...`);
190
+ console.log(`Installation type: ${isGlobal ? "global" : "local"}`);
133
191
  console.log(`Detected package manager: ${packageManager}`);
134
- console.log(`Running: ${installCommand}`);
192
+ console.log(`Running: ${updateCommand}`);
135
193
 
136
- await execAsync(installCommand);
194
+ await execAsync(updateCommand);
137
195
 
138
196
  console.log("✅ Update complete! Please restart your application.");
139
197
  } catch (error) {
140
- const manualCommand = getInstallCommand(packageManager, PACKAGE_NAME);
198
+ const manualCommand = getUpdateCommand(packageManager, PACKAGE_NAME, isGlobal);
141
199
  console.error("❌ Failed to update MindSim SDK.");
142
200
  if (error instanceof Error) {
143
201
  console.error(error.message);