@exchanet/enet 1.0.6 → 1.0.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.
@@ -1,13 +1,13 @@
1
- # METHOD REFLEX — Agent Instructions
1
+ # METHOD MODULAR DESIGN — Agent Instructions
2
2
  > Universal Modular Architecture · v1.0.0 · @exchanet
3
3
 
4
4
  ---
5
5
 
6
6
  ## YOUR PRIMARY DIRECTIVE / TU DIRECTIVA PRINCIPAL
7
7
 
8
- You are building a modular application using Method REFLEX. Every module you create must be self-describing via a `manifest.json` file. The manifest is written **before any code**. The system reads manifests to automatically generate the Admin Panel, dashboard, and settings UI. You never manually wire backend logic to the Admin Panel — the manifest does it.
8
+ You are building a modular application using Method Modular Design. Every module you create must be self-describing via a `manifest.json` file. The manifest is written **before any code**. The system reads manifests to automatically generate the Admin Panel, dashboard, and settings UI. You never manually wire backend logic to the Admin Panel — the manifest does it.
9
9
 
10
- Estás construyendo una aplicación modular usando Method REFLEX. Cada módulo que crees debe ser auto-descriptivo a través de un archivo `manifest.json`. El manifest se escribe **antes de cualquier código**. El sistema lee los manifests para generar automáticamente el Panel Admin, dashboard y UI de configuración. Nunca cablearás manualmente la lógica backend al Panel Admin — el manifest lo hace.
10
+ Estás construyendo una aplicación modular usando Method Modular Design. Cada módulo que crees debe ser auto-descriptivo a través de un archivo `manifest.json`. El manifest se escribe **antes de cualquier código**. El sistema lee los manifests para generar automáticamente el Panel Admin, dashboard y UI de configuración. Nunca cablearás manualmente la lógica backend al Panel Admin — el manifest lo hace.
11
11
 
12
12
  **If a module has no manifest, it does not exist.**
13
13
  **Si un módulo no tiene manifest, no existe.**
@@ -133,4 +133,4 @@ Before marking any module complete / Antes de marcar cualquier módulo como comp
133
133
  ---
134
134
 
135
135
  *For full documentation / Para documentación completa: `METHOD.md`*
136
- *Method REFLEX v1.0.0 · @exchanet · MIT*
136
+ *Method Modular Design v1.0.0 · @exchanet · MIT*
@@ -0,0 +1,191 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/exchanet/method-reflex/manifest.schema.json",
4
+ "title": "Method REFLEX — Module Manifest",
5
+ "description": "Universal contract for self-describing modules. Every module must have a valid manifest.",
6
+ "type": "object",
7
+ "required": ["id", "name", "version", "type", "section"],
8
+ "additionalProperties": false,
9
+
10
+ "definitions": {
11
+ "handlerRef": {
12
+ "type": "string",
13
+ "pattern": "^[A-Z][a-zA-Z0-9]*\\.[a-z][a-zA-Z0-9]*$",
14
+ "description": "ClassName.methodName — e.g. ActivityLogger.getAll"
15
+ },
16
+ "capabilityType": {
17
+ "type": "string",
18
+ "enum": ["view", "action", "metric", "widget", "page", "theme", "component-override"]
19
+ },
20
+ "uiWidget": {
21
+ "type": "string",
22
+ "enum": ["text", "textarea", "number", "slider", "toggle", "select", "multiselect", "code", "color", "date", "tags"]
23
+ },
24
+ "settingType": {
25
+ "type": "string",
26
+ "enum": ["integer", "number", "string", "boolean", "select", "multiselect"]
27
+ }
28
+ },
29
+
30
+ "properties": {
31
+ "id": {
32
+ "type": "string",
33
+ "pattern": "^[a-z][a-z0-9-]*$",
34
+ "minLength": 2,
35
+ "maxLength": 64,
36
+ "description": "Unique module identifier. kebab-case. Used in URLs, API paths, and settings namespace."
37
+ },
38
+
39
+ "name": {
40
+ "type": "string",
41
+ "minLength": 2,
42
+ "maxLength": 100,
43
+ "description": "Human-readable module name. Shown in Admin Panel navigation."
44
+ },
45
+
46
+ "version": {
47
+ "type": "string",
48
+ "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
49
+ "description": "Semantic version: major.minor.patch"
50
+ },
51
+
52
+ "type": {
53
+ "type": "string",
54
+ "enum": ["core", "functional", "integration", "ui"],
55
+ "description": "core: system-level. functional: business logic. integration: external services. ui: themes and components."
56
+ },
57
+
58
+ "section": {
59
+ "type": "string",
60
+ "pattern": "^[a-z][a-z0-9-]*$",
61
+ "description": "Admin Panel navigation group. Modules with the same section are grouped together."
62
+ },
63
+
64
+ "description": {
65
+ "type": "string",
66
+ "maxLength": 500,
67
+ "description": "Optional. Human-readable description of the module."
68
+ },
69
+
70
+ "icon": {
71
+ "type": "string",
72
+ "description": "Optional. Icon name from the project's icon library."
73
+ },
74
+
75
+ "dependencies": {
76
+ "type": "array",
77
+ "items": {
78
+ "type": "string",
79
+ "pattern": "^[a-z][a-z0-9-]*$"
80
+ },
81
+ "description": "Module IDs that must be booted before this module."
82
+ },
83
+
84
+ "hooks": {
85
+ "type": "object",
86
+ "patternProperties": {
87
+ "^[a-z][a-z0-9]*(\\.[a-z][a-z0-9]*)*$": {
88
+ "$ref": "#/definitions/handlerRef"
89
+ }
90
+ },
91
+ "additionalProperties": false,
92
+ "description": "Events this module listens to. Format: { 'event.name': 'ClassName.method' }"
93
+ },
94
+
95
+ "settings": {
96
+ "type": "object",
97
+ "patternProperties": {
98
+ "^[a-z][a-z0-9_]*$": {
99
+ "type": "object",
100
+ "required": ["type", "label", "default"],
101
+ "additionalProperties": false,
102
+ "properties": {
103
+ "type": { "$ref": "#/definitions/settingType" },
104
+ "label": { "type": "string" },
105
+ "description": { "type": "string" },
106
+ "default": {},
107
+ "ui": { "$ref": "#/definitions/uiWidget" },
108
+ "options": {
109
+ "type": "array",
110
+ "items": {
111
+ "oneOf": [
112
+ { "type": "string" },
113
+ {
114
+ "type": "object",
115
+ "required": ["label", "value"],
116
+ "properties": {
117
+ "label": { "type": "string" },
118
+ "value": {}
119
+ }
120
+ }
121
+ ]
122
+ },
123
+ "description": "Required when type is select or multiselect."
124
+ },
125
+ "min": { "type": "number" },
126
+ "max": { "type": "number" },
127
+ "required": { "type": "boolean", "default": false }
128
+ }
129
+ }
130
+ },
131
+ "additionalProperties": false,
132
+ "description": "Configuration fields. Auto-rendered as forms in Admin Panel. Read via context.settings.get('key'). Never hardcode."
133
+ },
134
+
135
+ "capabilities": {
136
+ "type": "array",
137
+ "minItems": 0,
138
+ "items": {
139
+ "type": "object",
140
+ "required": ["type", "label"],
141
+ "additionalProperties": false,
142
+ "properties": {
143
+ "type": { "$ref": "#/definitions/capabilityType" },
144
+ "label": { "type": "string" },
145
+ "description": { "type": "string" },
146
+ "icon": { "type": "string" },
147
+ "data": {
148
+ "$ref": "#/definitions/handlerRef",
149
+ "description": "Handler that returns data. Required for: view, metric, widget."
150
+ },
151
+ "handler": {
152
+ "$ref": "#/definitions/handlerRef",
153
+ "description": "Handler that executes an operation. Required for: action."
154
+ },
155
+ "component": {
156
+ "type": "string",
157
+ "pattern": "^[A-Z][a-zA-Z0-9]*$",
158
+ "description": "Custom UI component name. For widget and page types."
159
+ },
160
+ "route": {
161
+ "type": "string",
162
+ "pattern": "^/[a-z0-9-/]*$",
163
+ "description": "URL route for page capabilities. Relative to /admin/{module-id}/"
164
+ },
165
+ "dangerous": {
166
+ "type": "boolean",
167
+ "default": false,
168
+ "description": "If true, Admin Panel shows a confirmation dialog before executing."
169
+ },
170
+ "permissions": {
171
+ "type": "array",
172
+ "items": {
173
+ "type": "string",
174
+ "pattern": "^[a-z][a-z0-9-]*:[a-z][a-z0-9-]*$"
175
+ },
176
+ "description": "Required permissions. Format: 'scope:action' e.g. 'users:delete'"
177
+ },
178
+ "target": {
179
+ "type": "string",
180
+ "description": "For component-override: the component name to replace."
181
+ },
182
+ "variables": {
183
+ "type": "object",
184
+ "description": "For theme capabilities: CSS custom property values."
185
+ }
186
+ }
187
+ },
188
+ "description": "What this module exposes. Each capability becomes a UI element in Admin Panel or dashboard."
189
+ }
190
+ }
191
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exchanet/enet",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "enet — exchanet methods manager. Install, scaffold and manage AI coding methods.",
5
5
  "bin": {
6
6
  "enet": "src/index.js"
@@ -25,13 +25,17 @@
25
25
  },
26
26
  "keywords": [
27
27
  "exchanet",
28
- "method-reflex",
28
+ "method-modular-design",
29
+ "method-enterprise-builder",
30
+ "method-iris",
29
31
  "method-pdca-t",
32
+ "military-grade",
30
33
  "modular-architecture",
31
34
  "vibe-coding",
32
35
  "cursor",
33
36
  "windsurf",
34
37
  "copilot",
38
+ "claudecode",
35
39
  "ai-coding"
36
40
  ],
37
41
  "author": "Francisco J Bernades (@exchanet)",
package/registry.json CHANGED
@@ -25,8 +25,8 @@
25
25
  "id": "pdca-t",
26
26
  "name": "Method PDCA-T",
27
27
  "description": "≥99% test coverage, zero vulnerabilities, systematic quality validation cycles. Use alongside Modular Design for pro-grade results.",
28
- "repo": "exchanet/method_pdca-t_coding_Cursor",
29
- "version": "1.0.0",
28
+ "repo": "exchanet/method_pdca-t_coding",
29
+ "version": "3.0.0",
30
30
  "tags": ["quality", "testing", "security", "coverage", "validation"],
31
31
  "adapters": {
32
32
  "cursor": ".cursor/rules/METHOD-PDCA-T.md",
@@ -2,11 +2,11 @@ import chalk from 'chalk'
2
2
  import ora from 'ora'
3
3
  import fs from 'fs-extra'
4
4
  import path from 'path'
5
- import { detectAgent, getInstallPath, AGENTS } from '../utils/agent-detector.js'
6
- import { getMethod } from '../utils/registry.js'
7
- import { fetchFromGitHub } from '../utils/registry.js'
5
+ import { detectSystemAgents, getInstallPath, AGENTS } from '../utils/agent-detector.js'
6
+ import { getMethod, fetchFromGitHub } from '../utils/registry.js'
8
7
 
9
8
  export async function installCommand(methodId, options) {
9
+ // Load method from registry
10
10
  const spinner = ora('Fetching registry...').start()
11
11
  const method = await getMethod(methodId).catch(() => null)
12
12
  spinner.stop()
@@ -17,40 +17,92 @@ export async function installCommand(methodId, options) {
17
17
  process.exit(1)
18
18
  }
19
19
 
20
- // Detect or force agent
21
- let agent
20
+ // Determine target agents
21
+ let targetAgents = []
22
+
22
23
  if (options.agent) {
24
+ // --agent flag: install for a specific agent only
23
25
  if (!AGENTS[options.agent]) {
24
26
  console.log(chalk.red(` ✗ Unknown agent: "${options.agent}"`))
25
- console.log(chalk.dim(` Valid: cursor, windsurf, copilot, generic\n`))
27
+ console.log(chalk.dim(` Valid: ${Object.keys(AGENTS).filter(k => k !== 'generic').join(', ')}\n`))
26
28
  process.exit(1)
27
29
  }
28
- agent = { key: options.agent, ...AGENTS[options.agent] }
30
+ targetAgents = [{ key: options.agent, ...AGENTS[options.agent] }]
31
+
29
32
  } else {
30
- agent = await detectAgent()
33
+ // Detect all agents installed on the system
34
+ const detected = await detectSystemAgents()
35
+
36
+ if (detected.length === 0) {
37
+ console.log(chalk.yellow(' ⚠ No AI agents detected on this system.'))
38
+ console.log(chalk.dim(` Use ${chalk.white('--agent <name>')} to force an agent.`))
39
+ console.log(chalk.dim(` Valid: cursor, windsurf, antigravity, claudecode, copilot, generic\n`))
40
+ process.exit(1)
41
+ }
42
+
43
+ if (detected.length === 1 || options.global) {
44
+ // Only one detected, or --global flag: install for all without asking
45
+ targetAgents = detected
46
+ } else {
47
+ // Multiple agents detected: ask the user
48
+ console.log(chalk.white(`\n Detected ${detected.length} agents on this system:\n`))
49
+ detected.forEach((a, i) => console.log(chalk.dim(` [${i + 1}] ${a.name}`)))
50
+ console.log(chalk.dim(` [${detected.length + 1}] All of the above\n`))
51
+
52
+ const { createInterface } = await import('readline')
53
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
54
+ const answer = await new Promise(resolve => {
55
+ rl.question(chalk.white(' Install for which agent(s)? '), resolve)
56
+ })
57
+ rl.close()
58
+
59
+ const choice = parseInt(answer.trim())
60
+ if (choice === detected.length + 1) {
61
+ targetAgents = detected
62
+ } else if (choice >= 1 && choice <= detected.length) {
63
+ targetAgents = [detected[choice - 1]]
64
+ } else {
65
+ console.log(chalk.red('\n Invalid choice. Cancelled.\n'))
66
+ process.exit(1)
67
+ }
68
+ }
31
69
  }
32
70
 
33
- console.log(chalk.dim(` Method : ${chalk.white(method.name)}`))
34
- console.log(chalk.dim(` Agent : ${chalk.white(agent.name)}${options.agent ? '' : chalk.dim(' (auto-detected)')}`))
35
- console.log(chalk.dim(` Source : ${chalk.white(`github.com/${method.repo}`)}\n`))
71
+ console.log()
36
72
 
37
- const fetchSpinner = ora(`Fetching adapter...`).start()
73
+ // Install for each target agent
74
+ let schemaInstalled = false
75
+ for (const agent of targetAgents) {
76
+ await installForAgent(method, agent, options, schemaInstalled)
77
+ schemaInstalled = true // only install schema once
78
+ }
79
+
80
+ printHints(methodId)
81
+ }
82
+
83
+ async function installForAgent(method, agent, options, skipExtras = false) {
84
+ const isGlobal = options.global || false
85
+ const adapterKey = method.adapters[agent.key] ? agent.key : 'generic'
86
+ const adapterPath = method.adapters[adapterKey]
87
+
88
+ if (isGlobal && !agent.globalInstallDir) {
89
+ console.log(chalk.yellow(` ⚠ ${agent.name} does not support global install — skipping`))
90
+ return
91
+ }
92
+
93
+ const spinner = ora(`Installing for ${agent.name}${isGlobal ? ' (global)' : ''}...`).start()
38
94
 
39
95
  try {
40
- const adapterPath = method.adapters[agent.key] ?? method.adapters.generic
41
96
  const content = await fetchFromGitHub(method.repo, adapterPath)
42
-
43
- const installPath = options.global
44
- ? path.join(process.env.HOME || process.env.USERPROFILE, '.enet', `${methodId}.md`)
45
- : getInstallPath(agent, methodId)
97
+ const installPath = getInstallPath(agent, method.id, { global: isGlobal })
46
98
 
47
99
  await fs.ensureDir(path.dirname(installPath))
48
100
 
49
- // Windsurf appends, others overwrite
101
+ // Windsurf global appends to global_rules.md
50
102
  if (agent.key === 'windsurf' && await fs.pathExists(installPath)) {
51
103
  const existing = await fs.readFile(installPath, 'utf8')
52
104
  if (existing.includes(method.name)) {
53
- fetchSpinner.warn(chalk.yellow(`${method.name} already installed`))
105
+ spinner.warn(chalk.yellow(`${agent.name} already installed`))
54
106
  return
55
107
  }
56
108
  await fs.appendFile(installPath, `\n\n---\n\n${content}`)
@@ -58,32 +110,29 @@ export async function installCommand(methodId, options) {
58
110
  await fs.writeFile(installPath, content)
59
111
  }
60
112
 
61
- fetchSpinner.succeed(chalk.green(`${method.name} installed`))
62
- console.log(chalk.dim(` → ${path.relative(process.cwd(), installPath)}`))
113
+ spinner.succeed(chalk.green(`${agent.name} installed`))
114
+ console.log(chalk.dim(` → ${installPath}`))
63
115
  console.log(chalk.dim(` ${agent.configNote}\n`))
64
116
 
65
- // Download extras (e.g. manifest.schema.json for modular-design)
66
- if (method.extras) {
67
- for (const [key, filePath] of Object.entries(method.extras)) {
68
- const extraSpinner = ora(`Fetching ${key}...`).start()
69
- try {
70
- const extraContent = await fetchFromGitHub(method.repo, filePath)
71
- const extraOut = path.join(process.cwd(), path.basename(filePath))
72
- await fs.writeFile(extraOut, extraContent)
73
- extraSpinner.succeed(chalk.dim(`${key} → ${path.basename(filePath)}`))
74
- } catch {
75
- extraSpinner.warn(chalk.dim(`${key} not available (non-critical)`))
76
- }
117
+ } catch (err) {
118
+ spinner.fail(chalk.red(`${agent.name} — ${err.message}`))
119
+ return
120
+ }
121
+
122
+ // Download extras (schema, etc.) once
123
+ if (!skipExtras && method.extras) {
124
+ for (const [key, filePath] of Object.entries(method.extras)) {
125
+ const extraSpinner = ora(`Fetching ${key}...`).start()
126
+ try {
127
+ const extraContent = await fetchFromGitHub(method.repo, filePath)
128
+ const extraOut = path.join(process.cwd(), path.basename(filePath))
129
+ await fs.writeFile(extraOut, extraContent)
130
+ extraSpinner.succeed(chalk.dim(`${key} → ${path.basename(filePath)}`))
131
+ } catch {
132
+ extraSpinner.warn(chalk.dim(`${key} not available (non-critical)`))
77
133
  }
78
- console.log()
79
134
  }
80
-
81
- printHints(methodId)
82
-
83
- } catch (err) {
84
- fetchSpinner.fail(chalk.red(`Failed: ${err.message}`))
85
- console.log(chalk.dim(' Check your internet connection and try again.\n'))
86
- process.exit(1)
135
+ console.log()
87
136
  }
88
137
  }
89
138
 
@@ -95,7 +144,7 @@ function printHints(methodId) {
95
144
  console.log(chalk.dim(` 3. Agent builds Core → modules → Admin Panel`))
96
145
  console.log()
97
146
  console.log(chalk.dim(` ${chalk.white('enet new module <name>')} scaffold your first module`))
98
- console.log(chalk.dim(` ${chalk.white('enet validate')} check manifests at any time\n`))
147
+ console.log(chalk.dim(` ${chalk.white('enet validate')} check manifests at any time\n`))
99
148
  }
100
149
  if (methodId === 'pdca-t') {
101
150
  console.log(chalk.dim(` PDCA-T adds quality validation to your workflow.`))
@@ -1,70 +1,127 @@
1
1
  import fs from 'fs-extra'
2
2
  import path from 'path'
3
+ import os from 'os'
4
+
5
+ const HOME = os.homedir()
3
6
 
4
7
  export const AGENTS = {
5
8
  cursor: {
6
9
  name: 'Cursor',
7
- signals: ['.cursor/rules', '.cursor'],
8
- installDir: '.cursor/rules',
10
+ systemSignals: [
11
+ path.join(HOME, '.cursor')
12
+ ],
13
+ projectSignals: ['.cursor/rules', '.cursor'],
14
+ projectInstallDir: '.cursor/rules',
15
+ globalInstallDir: path.join(HOME, '.cursor', 'rules'),
9
16
  filename: 'enet-{id}.md',
10
17
  configNote: 'Rule auto-applies to all files (alwaysApply: true)'
11
18
  },
19
+ windsurf: {
20
+ name: 'Windsurf',
21
+ systemSignals: [
22
+ path.join(HOME, '.codeium', 'windsurf')
23
+ ],
24
+ projectSignals: ['.windsurfrules', '.windsurf'],
25
+ projectInstallDir: '.',
26
+ globalInstallDir: path.join(HOME, '.codeium', 'windsurf', 'memories'),
27
+ globalFilename: 'global_rules.md',
28
+ filename: '.windsurfrules',
29
+ configNote: 'Appended to global_rules.md'
30
+ },
12
31
  antigravity: {
13
32
  name: 'Antigravity (Google)',
14
- signals: ['.agent/rules', '.agent'],
15
- installDir: '.agent/rules',
33
+ systemSignals: [
34
+ path.join(HOME, '.gemini', 'antigravity')
35
+ ],
36
+ projectSignals: ['.agent/rules', '.agent'],
37
+ projectInstallDir: '.agent/rules',
38
+ globalInstallDir: path.join(HOME, '.gemini', 'antigravity', 'skills', 'method-modular-design'),
39
+ globalFilename: 'SKILL.md',
16
40
  filename: 'enet-{id}.md',
17
- configNote: 'Rule saved to .agent/rules/ — set activation to Always On in Antigravity'
41
+ configNote: 'Skill saved — set activation to Always On in Antigravity'
18
42
  },
19
43
  claudecode: {
20
44
  name: 'Claude Code',
21
- signals: ['CLAUDE.md', '.claude'],
22
- installDir: '.',
45
+ systemSignals: [
46
+ path.join(HOME, '.claude')
47
+ ],
48
+ projectSignals: ['CLAUDE.md', '.claude'],
49
+ projectInstallDir: '.',
50
+ globalInstallDir: path.join(HOME, '.claude'),
51
+ globalFilename: 'CLAUDE.md',
23
52
  filename: 'CLAUDE.md',
24
- configNote: 'Written to CLAUDE.md — Claude Code reads this file automatically'
25
- },
26
- windsurf: {
27
- name: 'Windsurf',
28
- signals: ['.windsurfrules', '.windsurf'],
29
- installDir: '.',
30
- filename: '.windsurfrules',
31
- configNote: 'Appended to .windsurfrules'
53
+ configNote: 'Written to ~/.claude/CLAUDE.md — Claude Code reads this automatically'
32
54
  },
33
55
  copilot: {
34
56
  name: 'GitHub Copilot',
35
- signals: ['.github/copilot-instructions.md'],
36
- installDir: '.github',
57
+ systemSignals: [],
58
+ projectSignals: ['.github/copilot-instructions.md'],
59
+ projectInstallDir: '.github',
60
+ globalInstallDir: null,
37
61
  filename: 'copilot-instructions.md',
38
62
  configNote: 'Written to .github/copilot-instructions.md'
39
63
  },
40
64
  generic: {
41
65
  name: 'Generic Agent',
42
- signals: [],
43
- installDir: '.enet',
66
+ systemSignals: [],
67
+ projectSignals: [],
68
+ projectInstallDir: '.enet',
69
+ globalInstallDir: null,
44
70
  filename: '{id}.md',
45
71
  configNote: 'Saved to .enet/ — paste contents into your agent\'s context'
46
72
  }
47
73
  }
48
74
 
49
75
  /**
50
- * Detects which AI agent is active in the current project.
76
+ * Detects ALL agents installed on the system by checking known global paths.
51
77
  */
52
- export async function detectAgent(cwd = process.cwd()) {
78
+ export async function detectSystemAgents() {
79
+ const found = []
53
80
  for (const [key, agent] of Object.entries(AGENTS)) {
54
81
  if (key === 'generic') continue
55
- for (const signal of agent.signals) {
82
+ for (const signal of agent.systemSignals) {
83
+ if (await fs.pathExists(signal)) {
84
+ found.push({ key, ...agent })
85
+ break
86
+ }
87
+ }
88
+ }
89
+ return found
90
+ }
91
+
92
+ /**
93
+ * Detects ALL agents present in the current project.
94
+ */
95
+ export async function detectProjectAgents(cwd = process.cwd()) {
96
+ const found = []
97
+ for (const [key, agent] of Object.entries(AGENTS)) {
98
+ if (key === 'generic') continue
99
+ for (const signal of agent.projectSignals) {
56
100
  if (await fs.pathExists(path.join(cwd, signal))) {
57
- return { key, ...agent }
101
+ found.push({ key, ...agent })
102
+ break
58
103
  }
59
104
  }
60
105
  }
61
- return { key: 'generic', ...AGENTS.generic }
106
+ return found
62
107
  }
63
108
 
64
109
  /**
65
- * Returns the full install path for a method adapter.
110
+ * Returns the first detected agent (legacy, used by status/doctor).
66
111
  */
67
- export function getInstallPath(agent, methodId, cwd = process.cwd()) {
112
+ export async function detectAgent(cwd = process.cwd()) {
113
+ const agents = await detectProjectAgents(cwd)
114
+ return agents[0] ?? { key: 'generic', ...AGENTS.generic }
115
+ }
116
+
117
+ /**
118
+ * Returns the install path for a method adapter.
119
+ */
120
+ export function getInstallPath(agent, methodId, { global = false, cwd = process.cwd() } = {}) {
121
+ if (global) {
122
+ const filename = agent.globalFilename ?? agent.filename.replace('{id}', methodId)
123
+ return path.join(agent.globalInstallDir, filename)
124
+ }
68
125
  const filename = agent.filename.replace('{id}', methodId)
69
- return path.join(cwd, agent.installDir, filename)
126
+ return path.join(cwd, agent.projectInstallDir, filename)
70
127
  }