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.
- package/dist/data/api.json +3758 -7222
- package/dist/src/core/build-pet.d.ts.map +1 -1
- package/dist/src/core/build-pet.js +7 -0
- package/dist/src/core/build-pet.js.map +1 -1
- package/dist/src/core/cli.js +456 -130
- package/dist/src/core/cli.js.map +1 -1
- package/dist/src/core/ensure-npmignore.d.ts +30 -0
- package/dist/src/core/ensure-npmignore.d.ts.map +1 -0
- package/dist/src/core/ensure-npmignore.js +121 -0
- package/dist/src/core/ensure-npmignore.js.map +1 -0
- package/dist/src/core/index.d.ts +6 -3
- package/dist/src/core/index.d.ts.map +1 -1
- package/dist/src/core/index.js +9 -3
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/core/mcp-generator.d.ts +56 -0
- package/dist/src/core/mcp-generator.d.ts.map +1 -0
- package/dist/src/core/mcp-generator.js +1438 -0
- package/dist/src/core/mcp-generator.js.map +1 -0
- package/dist/src/core/mcp-server.js +0 -0
- package/dist/src/core/openapi-generator.d.ts +59 -0
- package/dist/src/core/openapi-generator.d.ts.map +1 -0
- package/dist/src/core/openapi-generator.js +800 -0
- package/dist/src/core/openapi-generator.js.map +1 -0
- package/dist/src/core/pet-config.d.ts +107 -49
- package/dist/src/core/pet-config.d.ts.map +1 -1
- package/dist/src/core/pet-config.js +6 -4
- package/dist/src/core/pet-config.js.map +1 -1
- package/dist/src/core/pet-downloader.d.ts +16 -0
- package/dist/src/core/pet-downloader.d.ts.map +1 -1
- package/dist/src/core/pet-downloader.js +145 -3
- package/dist/src/core/pet-downloader.js.map +1 -1
- package/dist/src/core/publish-pet.d.ts +29 -0
- package/dist/src/core/publish-pet.d.ts.map +1 -0
- package/dist/src/core/publish-pet.js +372 -0
- package/dist/src/core/publish-pet.js.map +1 -0
- package/dist/src/core/sdk-generator.d.ts +92 -0
- package/dist/src/core/sdk-generator.d.ts.map +1 -0
- package/dist/src/core/sdk-generator.js +567 -0
- package/dist/src/core/sdk-generator.js.map +1 -0
- package/dist/src/core/search-pets.d.ts +5 -0
- package/dist/src/core/search-pets.d.ts.map +1 -1
- package/dist/src/core/search-pets.js +43 -0
- package/dist/src/core/search-pets.js.map +1 -1
- package/dist/src/core/security-scanner.d.ts +49 -0
- package/dist/src/core/security-scanner.d.ts.map +1 -0
- package/dist/src/core/security-scanner.js +255 -0
- package/dist/src/core/security-scanner.js.map +1 -0
- package/dist/src/core/tool-lister.d.ts +61 -0
- package/dist/src/core/tool-lister.d.ts.map +1 -0
- package/dist/src/core/tool-lister.js +333 -0
- package/dist/src/core/tool-lister.js.map +1 -0
- package/dist/src/core/validate-pet.d.ts +2 -0
- package/dist/src/core/validate-pet.d.ts.map +1 -1
- package/dist/src/core/validate-pet.js +93 -1
- package/dist/src/core/validate-pet.js.map +1 -1
- package/dist/src/sdk/plugin-factory.d.ts +86 -0
- package/dist/src/sdk/plugin-factory.d.ts.map +1 -1
- package/dist/src/sdk/plugin-factory.js +450 -53
- package/dist/src/sdk/plugin-factory.js.map +1 -1
- package/dist/src/sdk/prompts-manager.d.ts +6 -0
- package/dist/src/sdk/prompts-manager.d.ts.map +1 -0
- package/dist/src/sdk/prompts-manager.js +162 -0
- package/dist/src/sdk/prompts-manager.js.map +1 -0
- package/package.json +1 -1
- package/dist/src/core/local-cache.d.ts +0 -69
- package/dist/src/core/local-cache.d.ts.map +0 -1
- package/dist/src/core/local-cache.js +0 -212
- package/dist/src/core/local-cache.js.map +0 -1
- package/dist/src/core/plugin-factory.d.ts +0 -58
- package/dist/src/core/plugin-factory.d.ts.map +0 -1
- package/dist/src/core/plugin-factory.js +0 -212
- 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
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
201
|
-
|
|
305
|
+
envDirsFound: envDirs.length,
|
|
306
|
+
petsConfigDirsFound: petsConfigDirs.length,
|
|
307
|
+
projectRootsFound: projectRoots.length
|
|
202
308
|
});
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
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 .
|
|
391
|
-
|
|
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
|
|
622
|
+
logger.debug(`Synced to _global: ${key}`);
|
|
468
623
|
}
|
|
469
624
|
}
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
*
|
|
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
|
|
508
|
-
|
|
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
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
|
518
|
-
const content = readFileSync(
|
|
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
|
|
924
|
+
// Append .pets/ to exclude
|
|
528
925
|
const newContent = content.endsWith('\n') ? content : content + '\n';
|
|
529
|
-
writeFileSync(
|
|
530
|
-
logger.info("Added .pets/ to .
|
|
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 .
|
|
930
|
+
logger.debug(".pets/ already in .git/info/exclude");
|
|
534
931
|
}
|
|
535
932
|
}
|
|
536
933
|
catch (error) {
|
|
537
|
-
logger.warn(`Could not update .
|
|
934
|
+
logger.warn(`Could not update .git/info/exclude: ${error.message}`);
|
|
538
935
|
}
|
|
539
936
|
}
|
|
540
937
|
//# sourceMappingURL=plugin-factory.js.map
|