openpets 1.0.11 → 1.0.12

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.
Files changed (72) hide show
  1. package/dist/data/api.json +3758 -7222
  2. package/dist/src/core/build-pet.d.ts.map +1 -1
  3. package/dist/src/core/build-pet.js +7 -0
  4. package/dist/src/core/build-pet.js.map +1 -1
  5. package/dist/src/core/cli.js +456 -130
  6. package/dist/src/core/cli.js.map +1 -1
  7. package/dist/src/core/ensure-npmignore.d.ts +30 -0
  8. package/dist/src/core/ensure-npmignore.d.ts.map +1 -0
  9. package/dist/src/core/ensure-npmignore.js +121 -0
  10. package/dist/src/core/ensure-npmignore.js.map +1 -0
  11. package/dist/src/core/index.d.ts +6 -3
  12. package/dist/src/core/index.d.ts.map +1 -1
  13. package/dist/src/core/index.js +9 -3
  14. package/dist/src/core/index.js.map +1 -1
  15. package/dist/src/core/mcp-generator.d.ts +56 -0
  16. package/dist/src/core/mcp-generator.d.ts.map +1 -0
  17. package/dist/src/core/mcp-generator.js +1438 -0
  18. package/dist/src/core/mcp-generator.js.map +1 -0
  19. package/dist/src/core/mcp-server.js +0 -0
  20. package/dist/src/core/openapi-generator.d.ts +59 -0
  21. package/dist/src/core/openapi-generator.d.ts.map +1 -0
  22. package/dist/src/core/openapi-generator.js +800 -0
  23. package/dist/src/core/openapi-generator.js.map +1 -0
  24. package/dist/src/core/pet-config.d.ts +107 -49
  25. package/dist/src/core/pet-config.d.ts.map +1 -1
  26. package/dist/src/core/pet-config.js +6 -4
  27. package/dist/src/core/pet-config.js.map +1 -1
  28. package/dist/src/core/pet-downloader.d.ts +16 -0
  29. package/dist/src/core/pet-downloader.d.ts.map +1 -1
  30. package/dist/src/core/pet-downloader.js +145 -3
  31. package/dist/src/core/pet-downloader.js.map +1 -1
  32. package/dist/src/core/publish-pet.d.ts +29 -0
  33. package/dist/src/core/publish-pet.d.ts.map +1 -0
  34. package/dist/src/core/publish-pet.js +372 -0
  35. package/dist/src/core/publish-pet.js.map +1 -0
  36. package/dist/src/core/sdk-generator.d.ts +92 -0
  37. package/dist/src/core/sdk-generator.d.ts.map +1 -0
  38. package/dist/src/core/sdk-generator.js +567 -0
  39. package/dist/src/core/sdk-generator.js.map +1 -0
  40. package/dist/src/core/search-pets.d.ts +5 -0
  41. package/dist/src/core/search-pets.d.ts.map +1 -1
  42. package/dist/src/core/search-pets.js +43 -0
  43. package/dist/src/core/search-pets.js.map +1 -1
  44. package/dist/src/core/security-scanner.d.ts +49 -0
  45. package/dist/src/core/security-scanner.d.ts.map +1 -0
  46. package/dist/src/core/security-scanner.js +255 -0
  47. package/dist/src/core/security-scanner.js.map +1 -0
  48. package/dist/src/core/tool-lister.d.ts +61 -0
  49. package/dist/src/core/tool-lister.d.ts.map +1 -0
  50. package/dist/src/core/tool-lister.js +333 -0
  51. package/dist/src/core/tool-lister.js.map +1 -0
  52. package/dist/src/core/validate-pet.d.ts +2 -0
  53. package/dist/src/core/validate-pet.d.ts.map +1 -1
  54. package/dist/src/core/validate-pet.js +93 -1
  55. package/dist/src/core/validate-pet.js.map +1 -1
  56. package/dist/src/sdk/plugin-factory.d.ts +86 -0
  57. package/dist/src/sdk/plugin-factory.d.ts.map +1 -1
  58. package/dist/src/sdk/plugin-factory.js +450 -53
  59. package/dist/src/sdk/plugin-factory.js.map +1 -1
  60. package/dist/src/sdk/prompts-manager.d.ts +6 -0
  61. package/dist/src/sdk/prompts-manager.d.ts.map +1 -0
  62. package/dist/src/sdk/prompts-manager.js +162 -0
  63. package/dist/src/sdk/prompts-manager.js.map +1 -0
  64. package/package.json +1 -1
  65. package/dist/src/core/local-cache.d.ts +0 -69
  66. package/dist/src/core/local-cache.d.ts.map +0 -1
  67. package/dist/src/core/local-cache.js +0 -212
  68. package/dist/src/core/local-cache.js.map +0 -1
  69. package/dist/src/core/plugin-factory.d.ts +0 -58
  70. package/dist/src/core/plugin-factory.d.ts.map +0 -1
  71. package/dist/src/core/plugin-factory.js +0 -212
  72. package/dist/src/core/plugin-factory.js.map +0 -1
@@ -3,6 +3,7 @@ import { readFileSync, existsSync } from "fs";
3
3
  import { resolve, dirname, join } from "path";
4
4
  import { createLogger } from "./logger";
5
5
  import { config } from "dotenv";
6
+ import { ensurePromptsFolder } from "./prompts-manager";
6
7
  import * as zodRuntime from "zod";
7
8
  /**
8
9
  * Find the git root directory by walking up from the current directory
@@ -23,6 +24,103 @@ function findGitRoot(startDir) {
23
24
  }
24
25
  return null;
25
26
  }
27
+ /**
28
+ * Find all directories containing .env files by traversing up from startDir
29
+ * Returns array of directories sorted from closest to furthest
30
+ */
31
+ function findEnvDirs(startDir) {
32
+ const envDirs = [];
33
+ let currentDir = resolve(startDir);
34
+ const root = resolve("/");
35
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
36
+ while (currentDir !== root) {
37
+ // Stop at home directory to avoid going too far up
38
+ if (currentDir === homeDir) {
39
+ // Still check home directory for .env
40
+ if (existsSync(join(currentDir, ".env"))) {
41
+ envDirs.push(currentDir);
42
+ }
43
+ break;
44
+ }
45
+ // Check if this directory has a .env file
46
+ if (existsSync(join(currentDir, ".env"))) {
47
+ envDirs.push(currentDir);
48
+ }
49
+ const parentDir = dirname(currentDir);
50
+ if (parentDir === currentDir)
51
+ break;
52
+ currentDir = parentDir;
53
+ }
54
+ return envDirs;
55
+ }
56
+ /**
57
+ * Find all directories containing .pets/config.json by traversing up from startDir
58
+ * Returns array of directories sorted from closest to furthest
59
+ */
60
+ function findPetsConfigDirs(startDir) {
61
+ const configDirs = [];
62
+ let currentDir = resolve(startDir);
63
+ const root = resolve("/");
64
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
65
+ while (currentDir !== root) {
66
+ // Stop at home directory
67
+ if (currentDir === homeDir) {
68
+ if (existsSync(join(currentDir, ".pets", "config.json"))) {
69
+ configDirs.push(currentDir);
70
+ }
71
+ break;
72
+ }
73
+ // Check if this directory has a .pets/config.json file
74
+ if (existsSync(join(currentDir, ".pets", "config.json"))) {
75
+ configDirs.push(currentDir);
76
+ }
77
+ const parentDir = dirname(currentDir);
78
+ if (parentDir === currentDir)
79
+ break;
80
+ currentDir = parentDir;
81
+ }
82
+ return configDirs;
83
+ }
84
+ /**
85
+ * Find potential project roots by looking for common project indicators
86
+ * Returns directories containing opencode.json, package.json, or .git
87
+ */
88
+ function findProjectRoots(startDir) {
89
+ const projectRoots = [];
90
+ let currentDir = resolve(startDir);
91
+ const root = resolve("/");
92
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
93
+ // Project indicators to look for
94
+ const projectIndicators = [
95
+ "opencode.json",
96
+ ".opencode",
97
+ "package.json",
98
+ ".git",
99
+ "Cargo.toml",
100
+ "go.mod",
101
+ "pyproject.toml",
102
+ "Gemfile"
103
+ ];
104
+ while (currentDir !== root) {
105
+ // Stop at home directory
106
+ if (currentDir === homeDir)
107
+ break;
108
+ // Check if this directory has any project indicator
109
+ for (const indicator of projectIndicators) {
110
+ if (existsSync(join(currentDir, indicator))) {
111
+ if (!projectRoots.includes(currentDir)) {
112
+ projectRoots.push(currentDir);
113
+ }
114
+ break;
115
+ }
116
+ }
117
+ const parentDir = dirname(currentDir);
118
+ if (parentDir === currentDir)
119
+ break;
120
+ currentDir = parentDir;
121
+ }
122
+ return projectRoots;
123
+ }
26
124
  // ============================================================================
27
125
  // CRITICAL: SCHEMA FORMAT REQUIREMENTS
28
126
  // ============================================================================
@@ -61,6 +159,8 @@ export { tool };
61
159
  export { zodRuntime as z };
62
160
  const logger = createLogger("plugin-factory");
63
161
  export function createPlugin(tools) {
162
+ // Ensure prompts folder is available before any tools are run
163
+ ensurePromptsFolder();
64
164
  const toolRecord = {};
65
165
  for (const toolDef of tools) {
66
166
  const schema = toolDef.schema;
@@ -188,29 +288,64 @@ export function loadEnv(petId) {
188
288
  const envVars = {};
189
289
  try {
190
290
  const cwd = resolve(process.cwd());
291
+ // Find all directories with .env files by traversing up from cwd
292
+ // This handles the case where plugins run from ~/.config/opencode
293
+ // but the user's project is elsewhere
294
+ const envDirs = findEnvDirs(cwd);
295
+ const petsConfigDirs = findPetsConfigDirs(cwd);
296
+ const projectRoots = findProjectRoots(cwd);
191
297
  const gitRoot = findGitRoot(cwd);
192
- // Determine paths to check - prioritize cwd, fall back to git root
193
- const cwdDotenvPath = resolve(cwd, '.env');
194
- const cwdPetsConfigPath = resolve(cwd, '.pets', 'config.json');
195
- const gitRootDotenvPath = gitRoot ? resolve(gitRoot, '.env') : null;
196
- const gitRootPetsConfigPath = gitRoot ? resolve(gitRoot, '.pets', 'config.json') : null;
298
+ // Ensure .pets/ is in .git/info/exclude (local excludes, not tracked)
299
+ if (gitRoot) {
300
+ ensurePetsInGitExclude(gitRoot);
301
+ }
197
302
  logger.debug(`Loading env vars for pet: ${petId || 'global'}`, {
198
303
  cwd,
199
304
  gitRoot,
200
- cwdDotenvExists: existsSync(cwdDotenvPath),
201
- gitRootDotenvExists: gitRootDotenvPath ? existsSync(gitRootDotenvPath) : false
305
+ envDirsFound: envDirs.length,
306
+ petsConfigDirsFound: petsConfigDirs.length,
307
+ projectRootsFound: projectRoots.length
202
308
  });
203
- // Load from .pets/config.json - check git root first, then cwd (cwd takes precedence)
204
- const petsConfigPaths = [gitRootPetsConfigPath, cwdPetsConfigPath].filter(Boolean);
205
- for (const petsConfigPath of petsConfigPaths) {
206
- if (petId && existsSync(petsConfigPath)) {
309
+ // Build list of all directories to check for .pets/config.json
310
+ // Priority: closest first (cwd), then parent dirs, then git root
311
+ const allConfigDirs = new Set();
312
+ // Add petsConfigDirs in reverse order (furthest first, so closest overwrites)
313
+ for (let i = petsConfigDirs.length - 1; i >= 0; i--) {
314
+ allConfigDirs.add(petsConfigDirs[i]);
315
+ }
316
+ // Add project roots in reverse order
317
+ for (let i = projectRoots.length - 1; i >= 0; i--) {
318
+ allConfigDirs.add(projectRoots[i]);
319
+ }
320
+ // Add git root
321
+ if (gitRoot) {
322
+ allConfigDirs.add(gitRoot);
323
+ }
324
+ // Add cwd last so it takes highest priority
325
+ allConfigDirs.add(cwd);
326
+ // Load from .pets/config.json files - furthest first, closest last (closest takes precedence)
327
+ // Load order within each file: _global first, then pet-specific (pet-specific overrides _global)
328
+ for (const configDir of allConfigDirs) {
329
+ const petsConfigPath = resolve(configDir, '.pets', 'config.json');
330
+ if (existsSync(petsConfigPath)) {
207
331
  try {
208
332
  const petsConfig = JSON.parse(readFileSync(petsConfigPath, 'utf-8'));
209
- const petEnvConfig = petsConfig.envConfig?.[petId] || {};
210
- for (const [key, value] of Object.entries(petEnvConfig)) {
333
+ // First load _global config (applies to all pets)
334
+ const globalEnvConfig = petsConfig.envConfig?.["_global"] || {};
335
+ for (const [key, value] of Object.entries(globalEnvConfig)) {
211
336
  if (typeof value === 'string' && value.length > 0) {
212
337
  envVars[key] = value;
213
- logger.debug(`Loaded from ${petsConfigPath}: ${key}`);
338
+ logger.debug(`Loaded from ${petsConfigPath} (_global): ${key}`);
339
+ }
340
+ }
341
+ // Then load pet-specific config (overrides _global)
342
+ if (petId) {
343
+ const petEnvConfig = petsConfig.envConfig?.[petId] || {};
344
+ for (const [key, value] of Object.entries(petEnvConfig)) {
345
+ if (typeof value === 'string' && value.length > 0) {
346
+ envVars[key] = value;
347
+ logger.debug(`Loaded from ${petsConfigPath} (${petId}): ${key}`);
348
+ }
214
349
  }
215
350
  }
216
351
  }
@@ -219,9 +354,26 @@ export function loadEnv(petId) {
219
354
  }
220
355
  }
221
356
  }
222
- // Load from .env files - check git root first, then cwd (cwd takes precedence)
223
- const dotenvPaths = [gitRootDotenvPath, cwdDotenvPath].filter(Boolean);
224
- for (const dotenvPath of dotenvPaths) {
357
+ // Build list of all directories to check for .env files
358
+ // Priority: furthest first, closest last (closest takes precedence)
359
+ const allEnvDirs = new Set();
360
+ // Add envDirs in reverse order (furthest first)
361
+ for (let i = envDirs.length - 1; i >= 0; i--) {
362
+ allEnvDirs.add(envDirs[i]);
363
+ }
364
+ // Add project roots in reverse order
365
+ for (let i = projectRoots.length - 1; i >= 0; i--) {
366
+ allEnvDirs.add(projectRoots[i]);
367
+ }
368
+ // Add git root
369
+ if (gitRoot) {
370
+ allEnvDirs.add(gitRoot);
371
+ }
372
+ // Add cwd last so it takes highest priority
373
+ allEnvDirs.add(cwd);
374
+ // Load from .env files - furthest first, closest last (closest takes precedence)
375
+ for (const envDir of allEnvDirs) {
376
+ const dotenvPath = resolve(envDir, '.env');
225
377
  if (existsSync(dotenvPath)) {
226
378
  const dotenvResult = config({ path: dotenvPath });
227
379
  if (dotenvResult.parsed) {
@@ -264,6 +416,8 @@ export function setEnv(petId, envVars, projectDir) {
264
416
  mkdirSync(petsDir, { recursive: true });
265
417
  logger.debug(`Created .pets directory at ${petsDir}`);
266
418
  }
419
+ // Ensure .pets/ is in .git/info/exclude (local excludes, not tracked)
420
+ ensurePetsInGitExclude(projectRoot);
267
421
  // Load existing config or create new one
268
422
  let petsConfig = {
269
423
  enabled: [],
@@ -379,7 +533,6 @@ export function syncEnvToConfig(projectDir) {
379
533
  const dotenvPath = resolve(projectRoot, '.env');
380
534
  const petsDir = resolve(projectRoot, ".pets");
381
535
  const petsConfigPath = resolve(petsDir, "config.json");
382
- const gitignorePath = resolve(projectRoot, ".gitignore");
383
536
  const opencodeJsonPath = resolve(projectRoot, "opencode.json");
384
537
  logger.info(`Syncing .env to .pets/config.json in ${projectRoot}`);
385
538
  // Ensure .pets directory exists
@@ -387,8 +540,8 @@ export function syncEnvToConfig(projectDir) {
387
540
  mkdirSync(petsDir, { recursive: true });
388
541
  logger.debug(`Created .pets directory at ${petsDir}`);
389
542
  }
390
- // Ensure .pets/ is in .gitignore
391
- ensurePetsInGitignore(gitignorePath);
543
+ // Ensure .pets/ is in .git/info/exclude (local excludes, not tracked)
544
+ ensurePetsInGitExclude(projectRoot);
392
545
  // Load existing pets config or create new one
393
546
  let petsConfig = {
394
547
  enabled: [],
@@ -456,34 +609,22 @@ export function syncEnvToConfig(projectDir) {
456
609
  let syncedCount = 0;
457
610
  const syncedPets = [];
458
611
  // Create a "global" env config that applies to all pets
612
+ // We ONLY store in _global - pet-specific configs are for pet-specific overrides only
459
613
  if (!petsConfig.envConfig["_global"]) {
460
614
  petsConfig.envConfig["_global"] = {};
461
615
  }
462
- // Sync all .env variables to the global config
616
+ // Sync all .env variables to the global config only
617
+ // Do NOT copy to each pet - that causes massive duplication
463
618
  for (const [key, value] of Object.entries(envVars)) {
464
619
  if (value && value.length > 0) {
465
620
  petsConfig.envConfig["_global"][key] = value;
466
621
  syncedCount++;
467
- logger.debug(`Synced global: ${key}`);
622
+ logger.debug(`Synced to _global: ${key}`);
468
623
  }
469
624
  }
470
- // Also sync to each configured pet's specific config
471
- for (const petId of configuredPets) {
472
- if (!petsConfig.envConfig[petId]) {
473
- petsConfig.envConfig[petId] = {};
474
- }
475
- // Merge global config into pet-specific config
476
- // Pet-specific values (if any) take precedence over global
477
- const globalConfig = petsConfig.envConfig["_global"] || {};
478
- const petConfig = petsConfig.envConfig[petId] || {};
479
- petsConfig.envConfig[petId] = {
480
- ...globalConfig,
481
- ...petConfig,
482
- // .env values override everything
483
- ...envVars
484
- };
485
- syncedPets.push(petId);
486
- }
625
+ // Note: We no longer copy _global to each pet's config
626
+ // The loadEnv() function handles merging _global with pet-specific at runtime
627
+ syncedPets.push("_global");
487
628
  // Update timestamp
488
629
  petsConfig.last_updated = new Date().toISOString();
489
630
  // Write config
@@ -501,21 +642,277 @@ export function syncEnvToConfig(projectDir) {
501
642
  return { success: false, message: `Failed to sync: ${error.message}`, synced: 0, pets: [] };
502
643
  }
503
644
  }
645
+ // ============================================================================
646
+ // READ-ONLY MODE FUNCTIONALITY
647
+ // ============================================================================
648
+ // Read-only mode prevents write operations from being registered for a pet.
649
+ // This provides safety when users want to use a pet for read operations only.
650
+ //
651
+ // Configuration sources (in priority order, highest first):
652
+ // 1. Environment variable: {PETNAME}_READ_ONLY=true (e.g., ASANA_READ_ONLY=true)
653
+ // 2. Pet-specific config: .pets/config.json → petConfig.{petId}.readOnly: true
654
+ // 3. Global config: .pets/config.json → petConfig._global.readOnly: true
655
+ // ============================================================================
656
+ /**
657
+ * Check if a pet is configured in read-only mode.
658
+ *
659
+ * Read-only mode prevents write tools from being registered for the pet.
660
+ * This is useful when users want to ensure they can only read data, not modify it.
661
+ *
662
+ * Configuration is checked in the following order (first match wins):
663
+ * 1. Environment variable: {PETNAME}_READ_ONLY=true (e.g., ASANA_READ_ONLY=true)
664
+ * 2. Pet-specific config in .pets/config.json: petConfig.{petId}.readOnly
665
+ * 3. Global config in .pets/config.json: petConfig._global.readOnly
666
+ *
667
+ * @param petId - The pet identifier (e.g., "asana", "jira", "github")
668
+ * @returns true if read-only mode is enabled for this pet
669
+ *
670
+ * @example
671
+ * ```typescript
672
+ * import { isReadOnly, createPlugin, type ToolDefinition } from "openpets-sdk"
673
+ *
674
+ * export const MyPlugin = async () => {
675
+ * const readOnly = isReadOnly("my-plugin")
676
+ *
677
+ * const readTools: ToolDefinition[] = [
678
+ * { name: "my-plugin-list", ... },
679
+ * { name: "my-plugin-get", ... }
680
+ * ]
681
+ *
682
+ * const writeTools: ToolDefinition[] = [
683
+ * { name: "my-plugin-create", ... },
684
+ * { name: "my-plugin-update", ... }
685
+ * ]
686
+ *
687
+ * // Only include write tools if not in read-only mode
688
+ * const tools = readOnly ? readTools : [...readTools, ...writeTools]
689
+ *
690
+ * return createPlugin(tools)
691
+ * }
692
+ * ```
693
+ */
694
+ export function isReadOnly(petId) {
695
+ // 1. Check environment variable (highest priority)
696
+ // Normalize pet ID to uppercase with underscores for env var name
697
+ const envVarName = `${petId.toUpperCase().replace(/-/g, '_')}_READ_ONLY`;
698
+ const envValue = process.env[envVarName];
699
+ if (envValue !== undefined) {
700
+ const isEnabled = envValue.toLowerCase() === 'true' || envValue === '1';
701
+ logger.debug(`Read-only check for ${petId}: ENV ${envVarName}=${envValue} → ${isEnabled}`);
702
+ return isEnabled;
703
+ }
704
+ // 2. Check .pets/config.json (pet-specific, then global)
705
+ const petsConfig = loadPetsConfigForReadOnly();
706
+ if (petsConfig?.petConfig) {
707
+ // Check pet-specific setting first
708
+ const petSettings = petsConfig.petConfig[petId];
709
+ if (petSettings?.readOnly !== undefined) {
710
+ logger.debug(`Read-only check for ${petId}: petConfig.${petId}.readOnly=${petSettings.readOnly}`);
711
+ return petSettings.readOnly === true;
712
+ }
713
+ // Check global setting
714
+ const globalSettings = petsConfig.petConfig["_global"];
715
+ if (globalSettings?.readOnly !== undefined) {
716
+ logger.debug(`Read-only check for ${petId}: petConfig._global.readOnly=${globalSettings.readOnly}`);
717
+ return globalSettings.readOnly === true;
718
+ }
719
+ }
720
+ logger.debug(`Read-only check for ${petId}: no configuration found → false`);
721
+ return false;
722
+ }
723
+ /**
724
+ * Set read-only mode for a pet in .pets/config.json
725
+ *
726
+ * @param petId - The pet identifier (e.g., "asana", "jira"). Use "_global" for all pets.
727
+ * @param enabled - Whether read-only mode should be enabled
728
+ * @param projectDir - Optional project directory (defaults to cwd)
729
+ * @returns Result object with success status and message
730
+ */
731
+ export function setReadOnly(petId, enabled, projectDir) {
732
+ const { writeFileSync, existsSync, mkdirSync } = require("fs");
733
+ try {
734
+ const projectRoot = projectDir || resolve(process.cwd());
735
+ const petsDir = resolve(projectRoot, ".pets");
736
+ const petsConfigPath = resolve(petsDir, "config.json");
737
+ // Ensure .pets directory exists
738
+ if (!existsSync(petsDir)) {
739
+ mkdirSync(petsDir, { recursive: true });
740
+ logger.debug(`Created .pets directory at ${petsDir}`);
741
+ }
742
+ // Ensure .pets/ is in .git/info/exclude
743
+ ensurePetsInGitExclude(projectRoot);
744
+ // Load existing config or create new one
745
+ let petsConfig = {
746
+ enabled: [],
747
+ disabled: [],
748
+ envConfig: {},
749
+ petConfig: {}
750
+ };
751
+ if (existsSync(petsConfigPath)) {
752
+ try {
753
+ const existing = JSON.parse(readFileSync(petsConfigPath, "utf-8"));
754
+ petsConfig = {
755
+ enabled: existing.enabled || [],
756
+ disabled: existing.disabled || [],
757
+ envConfig: existing.envConfig || {},
758
+ petConfig: existing.petConfig || {},
759
+ ...existing
760
+ };
761
+ }
762
+ catch (error) {
763
+ logger.warn(`Could not parse existing .pets/config.json: ${error.message}`);
764
+ }
765
+ }
766
+ // Ensure petConfig structure exists
767
+ if (!petsConfig.petConfig) {
768
+ petsConfig.petConfig = {};
769
+ }
770
+ // Create or update pet-specific config
771
+ if (!petsConfig.petConfig[petId]) {
772
+ petsConfig.petConfig[petId] = {};
773
+ }
774
+ // Set the read-only value
775
+ petsConfig.petConfig[petId].readOnly = enabled;
776
+ // Update timestamp
777
+ petsConfig.last_updated = new Date().toISOString();
778
+ // Write config
779
+ writeFileSync(petsConfigPath, JSON.stringify(petsConfig, null, 2));
780
+ const modeStr = enabled ? "enabled" : "disabled";
781
+ const targetStr = petId === "_global" ? "all pets (global)" : `pet '${petId}'`;
782
+ logger.info(`Read-only mode ${modeStr} for ${targetStr}`);
783
+ return {
784
+ success: true,
785
+ message: `Read-only mode ${modeStr} for ${targetStr}`
786
+ };
787
+ }
788
+ catch (error) {
789
+ logger.error(`Failed to set read-only mode: ${error.message}`);
790
+ return { success: false, message: `Failed to set read-only mode: ${error.message}` };
791
+ }
792
+ }
793
+ /**
794
+ * Get read-only status for a pet or all pets
795
+ *
796
+ * @param petId - Optional pet identifier. If not provided, returns status for all configured pets.
797
+ * @param projectDir - Optional project directory (defaults to cwd)
798
+ * @returns Object with read-only status information
799
+ */
800
+ export function getReadOnlyStatus(petId, projectDir) {
801
+ const projectRoot = projectDir || resolve(process.cwd());
802
+ const petsConfig = loadPetsConfigForReadOnly(projectRoot);
803
+ const result = {
804
+ global: petsConfig?.petConfig?.["_global"]?.readOnly,
805
+ pets: {},
806
+ envOverrides: {}
807
+ };
808
+ // Get all configured pet IDs
809
+ const petIds = [];
810
+ if (petId) {
811
+ petIds.push(petId);
812
+ }
813
+ else if (petsConfig?.petConfig) {
814
+ petIds.push(...Object.keys(petsConfig.petConfig).filter(k => k !== "_global"));
815
+ }
816
+ // Check each pet's status
817
+ for (const pid of petIds) {
818
+ // Check config
819
+ const petSettings = petsConfig?.petConfig?.[pid];
820
+ if (petSettings?.readOnly !== undefined) {
821
+ result.pets[pid] = petSettings.readOnly;
822
+ }
823
+ // Check env override
824
+ const envVarName = `${pid.toUpperCase().replace(/-/g, '_')}_READ_ONLY`;
825
+ const envValue = process.env[envVarName];
826
+ if (envValue !== undefined) {
827
+ result.envOverrides[pid] = envValue.toLowerCase() === 'true' || envValue === '1';
828
+ }
829
+ }
830
+ return result;
831
+ }
832
+ /**
833
+ * Helper to filter tools based on read-only mode.
834
+ * Use this in your pet's plugin factory to easily filter out write tools.
835
+ *
836
+ * @param tools - Array of tool definitions
837
+ * @param writePatterns - Array of patterns that identify write operations (e.g., ['create', 'update', 'delete'])
838
+ * @param isReadOnlyMode - Whether read-only mode is enabled
839
+ * @param logger - Optional logger for debug output
840
+ * @returns Filtered array of tools
841
+ *
842
+ * @example
843
+ * ```typescript
844
+ * const WRITE_PATTERNS = ['create', 'update', 'delete', 'add-comment']
845
+ * const filteredTools = filterToolsForReadOnly(allTools, WRITE_PATTERNS, isReadOnly('my-pet'))
846
+ * ```
847
+ */
848
+ export function filterToolsForReadOnly(tools, writePatterns, isReadOnlyMode, debugLogger) {
849
+ if (!isReadOnlyMode) {
850
+ return tools;
851
+ }
852
+ const isWriteOperation = (toolName) => {
853
+ return writePatterns.some(pattern => toolName.includes(pattern));
854
+ };
855
+ const filteredTools = tools.filter(tool => !isWriteOperation(tool.name));
856
+ const excludedTools = tools.filter(tool => isWriteOperation(tool.name));
857
+ if (excludedTools.length > 0 && debugLogger) {
858
+ debugLogger.debug("Excluded write tools in read-only mode", {
859
+ excluded: excludedTools.map(t => t.name)
860
+ });
861
+ }
862
+ return filteredTools;
863
+ }
504
864
  /**
505
- * Ensure .pets/ is in .gitignore
865
+ * Internal helper to load .pets/config.json for read-only checks.
866
+ * This is separate from the main loadEnv to avoid circular dependencies.
506
867
  */
507
- function ensurePetsInGitignore(gitignorePath) {
508
- const { writeFileSync, existsSync, appendFileSync } = require("fs");
868
+ function loadPetsConfigForReadOnly(projectDir) {
869
+ try {
870
+ const cwd = projectDir || resolve(process.cwd());
871
+ const petsConfigDirs = findPetsConfigDirs(cwd);
872
+ // Check from closest to furthest
873
+ for (const configDir of petsConfigDirs) {
874
+ const petsConfigPath = resolve(configDir, '.pets', 'config.json');
875
+ if (existsSync(petsConfigPath)) {
876
+ const config = JSON.parse(readFileSync(petsConfigPath, 'utf-8'));
877
+ return config;
878
+ }
879
+ }
880
+ return null;
881
+ }
882
+ catch (error) {
883
+ logger.debug(`Could not load .pets/config.json: ${error.message}`);
884
+ return null;
885
+ }
886
+ }
887
+ /**
888
+ * Ensure .pets/ is in .git/info/exclude (local git excludes, not tracked)
889
+ * This avoids modifying .gitignore which would show up as a change
890
+ */
891
+ function ensurePetsInGitExclude(projectRoot) {
892
+ const { writeFileSync, existsSync, mkdirSync } = require("fs");
509
893
  const petsIgnorePattern = ".pets/";
894
+ const gitDir = join(projectRoot, ".git");
895
+ const gitInfoDir = join(gitDir, "info");
896
+ const excludePath = join(gitInfoDir, "exclude");
510
897
  try {
511
- if (!existsSync(gitignorePath)) {
512
- // Create .gitignore with .pets/
513
- writeFileSync(gitignorePath, `# OpenPets configuration (contains sensitive env vars)\n${petsIgnorePattern}\n`);
514
- logger.info("Created .gitignore with .pets/ entry");
898
+ // Only proceed if this is a git repository
899
+ if (!existsSync(gitDir)) {
900
+ logger.debug("Not a git repository, skipping exclude setup");
901
+ return;
902
+ }
903
+ // Ensure .git/info directory exists
904
+ if (!existsSync(gitInfoDir)) {
905
+ mkdirSync(gitInfoDir, { recursive: true });
906
+ logger.debug("Created .git/info directory");
907
+ }
908
+ if (!existsSync(excludePath)) {
909
+ // Create exclude file with .pets/
910
+ writeFileSync(excludePath, `# git ls-files --others --exclude-from=.git/info/exclude\n# Lines that start with '#' are comments.\n\n# OpenPets configuration (contains sensitive env vars)\n${petsIgnorePattern}\n`);
911
+ logger.info("Created .git/info/exclude with .pets/ entry");
515
912
  return;
516
913
  }
517
- // Check if .pets/ is already in .gitignore
518
- const content = readFileSync(gitignorePath, "utf-8");
914
+ // Check if .pets/ is already in exclude
915
+ const content = readFileSync(excludePath, "utf-8");
519
916
  const lines = content.split('\n');
520
917
  // Check for various patterns that would ignore .pets
521
918
  const petsPatterns = ['.pets/', '.pets', '/.pets/', '/.pets', '**/.pets/', '**/.pets'];
@@ -524,17 +921,17 @@ function ensurePetsInGitignore(gitignorePath) {
524
921
  return petsPatterns.includes(trimmed) || trimmed === '.pets' || trimmed === '.pets/';
525
922
  });
526
923
  if (!isIgnored) {
527
- // Append .pets/ to .gitignore
924
+ // Append .pets/ to exclude
528
925
  const newContent = content.endsWith('\n') ? content : content + '\n';
529
- writeFileSync(gitignorePath, newContent + `\n# OpenPets configuration (contains sensitive env vars)\n${petsIgnorePattern}\n`);
530
- logger.info("Added .pets/ to .gitignore");
926
+ writeFileSync(excludePath, newContent + `\n# OpenPets configuration (contains sensitive env vars)\n${petsIgnorePattern}\n`);
927
+ logger.info("Added .pets/ to .git/info/exclude");
531
928
  }
532
929
  else {
533
- logger.debug(".pets/ already in .gitignore");
930
+ logger.debug(".pets/ already in .git/info/exclude");
534
931
  }
535
932
  }
536
933
  catch (error) {
537
- logger.warn(`Could not update .gitignore: ${error.message}`);
934
+ logger.warn(`Could not update .git/info/exclude: ${error.message}`);
538
935
  }
539
936
  }
540
937
  //# sourceMappingURL=plugin-factory.js.map