@jinn-network/client 0.1.0 → 0.1.1
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/README.md +46 -9
- package/dist/adapters/mech/adapter.d.ts +1 -0
- package/dist/adapters/mech/adapter.js +35 -0
- package/dist/adapters/mech/adapter.js.map +1 -1
- package/dist/api/gather-status.js +1 -0
- package/dist/api/gather-status.js.map +1 -1
- package/dist/api/server.js +12 -0
- package/dist/api/server.js.map +1 -1
- package/dist/api/status-build.d.ts +1 -0
- package/dist/api/status-build.js.map +1 -1
- package/dist/api/status-rollup-build.d.ts +4 -0
- package/dist/api/status-rollup-build.js +4 -0
- package/dist/api/status-rollup-build.js.map +1 -1
- package/dist/bin/jinn-mcp.d.ts +14 -0
- package/dist/bin/jinn-mcp.js +19 -0
- package/dist/bin/jinn-mcp.js.map +1 -0
- package/dist/build-meta.json +1 -1
- package/dist/cli/commands/auth.d.ts +3 -0
- package/dist/cli/commands/auth.js +146 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/bootstrap.js +1 -0
- package/dist/cli/commands/bootstrap.js.map +1 -1
- package/dist/cli/commands/doctor.js +43 -11
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/fund-requirements.js +69 -1
- package/dist/cli/commands/fund-requirements.js.map +1 -1
- package/dist/cli/commands/history.js +1 -0
- package/dist/cli/commands/history.js.map +1 -1
- package/dist/cli/commands/init.js +31 -7
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/keys-backup.js +142 -10
- package/dist/cli/commands/keys-backup.js.map +1 -1
- package/dist/cli/commands/logs.js +28 -13
- package/dist/cli/commands/logs.js.map +1 -1
- package/dist/cli/commands/plugin-install.d.ts +3 -0
- package/dist/cli/commands/plugin-install.js +799 -0
- package/dist/cli/commands/plugin-install.js.map +1 -0
- package/dist/cli/commands/quickstart.d.ts +3 -0
- package/dist/cli/commands/quickstart.js +236 -0
- package/dist/cli/commands/quickstart.js.map +1 -0
- package/dist/cli/commands/run.js +6 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/stop.js +1 -0
- package/dist/cli/commands/stop.js.map +1 -1
- package/dist/cli/commands/submit-intent.js +11 -1
- package/dist/cli/commands/submit-intent.js.map +1 -1
- package/dist/cli/commands/update.d.ts +3 -0
- package/dist/cli/commands/update.js +154 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/version.js +15 -1
- package/dist/cli/commands/version.js.map +1 -1
- package/dist/cli/deployment-digest.js +20 -4
- package/dist/cli/deployment-digest.js.map +1 -1
- package/dist/cli/index.js +8 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/password.d.ts +15 -0
- package/dist/cli/password.js +29 -1
- package/dist/cli/password.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon/balance-topup-loop.d.ts +40 -0
- package/dist/daemon/balance-topup-loop.js +96 -0
- package/dist/daemon/balance-topup-loop.js.map +1 -0
- package/dist/daemon/daemon.d.ts +7 -0
- package/dist/daemon/daemon.js +12 -0
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/dashboard/index.html +500 -0
- package/dist/earning/bootstrap.d.ts +2 -0
- package/dist/earning/bootstrap.js +68 -14
- package/dist/earning/bootstrap.js.map +1 -1
- package/dist/earning/contracts.d.ts +17 -0
- package/dist/earning/contracts.js +7 -1
- package/dist/earning/contracts.js.map +1 -1
- package/dist/earning/faucet.d.ts +15 -0
- package/dist/earning/faucet.js +64 -0
- package/dist/earning/faucet.js.map +1 -0
- package/dist/earning/store.d.ts +5 -0
- package/dist/earning/store.js +7 -3
- package/dist/earning/store.js.map +1 -1
- package/dist/errors/unauthorized-account.d.ts +10 -0
- package/dist/errors/unauthorized-account.js +14 -0
- package/dist/errors/unauthorized-account.js.map +1 -0
- package/dist/main.js +30 -1
- package/dist/main.js.map +1 -1
- package/dist/mcp/operator-server.d.ts +34 -0
- package/dist/mcp/operator-server.js +219 -0
- package/dist/mcp/operator-server.js.map +1 -0
- package/dist/operator-errors.js +11 -0
- package/dist/operator-errors.js.map +1 -1
- package/dist/preflight/claude-auth.d.ts +57 -0
- package/dist/preflight/claude-auth.js +153 -0
- package/dist/preflight/claude-auth.js.map +1 -0
- package/dist/runner/claude.js +15 -0
- package/dist/runner/claude.js.map +1 -1
- package/dist/store/store.js +5 -0
- package/dist/store/store.js.map +1 -1
- package/dist/tx-retry.js +11 -1
- package/dist/tx-retry.js.map +1 -1
- package/package.json +12 -3
- package/skills/jinn-operator/SKILL.md +213 -0
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, copyFileSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { platform, homedir } from 'node:os';
|
|
7
|
+
import { emitResult } from '../output.js';
|
|
8
|
+
import { emitEnvelope } from '../../errors/envelope.js';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Skill content resolution
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const SKILL_DIR = fileURLToPath(new URL('../../../skills/jinn-operator', import.meta.url));
|
|
13
|
+
function loadSkillContent() {
|
|
14
|
+
return readFileSync(join(SKILL_DIR, 'SKILL.md'), 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
function stripFrontmatter(content) {
|
|
17
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n/);
|
|
18
|
+
if (!match)
|
|
19
|
+
return content;
|
|
20
|
+
return content.slice(match[0].length).trimStart();
|
|
21
|
+
}
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Helpers — JSON MCP config
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
function readJsonFile(filePath) {
|
|
26
|
+
if (!existsSync(filePath))
|
|
27
|
+
return {};
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function writeJsonFile(filePath, data) {
|
|
36
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
37
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
38
|
+
}
|
|
39
|
+
function hasJsonMcpServer(filePath, rootKey) {
|
|
40
|
+
const data = readJsonFile(filePath);
|
|
41
|
+
const section = data[rootKey];
|
|
42
|
+
return section !== undefined && 'jinn' in section;
|
|
43
|
+
}
|
|
44
|
+
function upsertJsonMcpServer(filePath, rootKey) {
|
|
45
|
+
const data = readJsonFile(filePath);
|
|
46
|
+
const section = (data[rootKey] ?? {});
|
|
47
|
+
if ('jinn' in section) {
|
|
48
|
+
return { ok: true, detail: `Already configured in ${filePath}` };
|
|
49
|
+
}
|
|
50
|
+
section['jinn'] = { command: 'jinn-mcp' };
|
|
51
|
+
data[rootKey] = section;
|
|
52
|
+
writeJsonFile(filePath, data);
|
|
53
|
+
return { ok: true, detail: `Wrote MCP entry to ${filePath}` };
|
|
54
|
+
}
|
|
55
|
+
function removeJsonMcpServer(filePath, rootKey) {
|
|
56
|
+
if (!existsSync(filePath)) {
|
|
57
|
+
return { ok: true, detail: 'Config file does not exist' };
|
|
58
|
+
}
|
|
59
|
+
const data = readJsonFile(filePath);
|
|
60
|
+
const section = (data[rootKey] ?? {});
|
|
61
|
+
if (!('jinn' in section)) {
|
|
62
|
+
return { ok: true, detail: 'Not configured' };
|
|
63
|
+
}
|
|
64
|
+
delete section['jinn'];
|
|
65
|
+
data[rootKey] = section;
|
|
66
|
+
writeJsonFile(filePath, data);
|
|
67
|
+
return { ok: true, detail: `Removed MCP entry from ${filePath}` };
|
|
68
|
+
}
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Helpers — TOML MCP config (Codex)
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
const TOML_BLOCK = `[mcp_servers.jinn]\ncommand = "jinn-mcp"`;
|
|
73
|
+
function hasTomlMcpServer(filePath) {
|
|
74
|
+
if (!existsSync(filePath))
|
|
75
|
+
return false;
|
|
76
|
+
return readFileSync(filePath, 'utf-8').includes('[mcp_servers.jinn]');
|
|
77
|
+
}
|
|
78
|
+
function upsertTomlMcpServer(filePath) {
|
|
79
|
+
if (hasTomlMcpServer(filePath)) {
|
|
80
|
+
return { ok: true, detail: `Already configured in ${filePath}` };
|
|
81
|
+
}
|
|
82
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
83
|
+
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
|
|
84
|
+
const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n\n' : existing.length > 0 ? '\n' : '';
|
|
85
|
+
writeFileSync(filePath, existing + separator + TOML_BLOCK + '\n', 'utf-8');
|
|
86
|
+
return { ok: true, detail: `Wrote MCP entry to ${filePath}` };
|
|
87
|
+
}
|
|
88
|
+
function removeTomlMcpServer(filePath) {
|
|
89
|
+
if (!existsSync(filePath)) {
|
|
90
|
+
return { ok: true, detail: 'Config file does not exist' };
|
|
91
|
+
}
|
|
92
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
93
|
+
if (!content.includes('[mcp_servers.jinn]')) {
|
|
94
|
+
return { ok: true, detail: 'Not configured' };
|
|
95
|
+
}
|
|
96
|
+
// Remove the block: header line + command line (and optional trailing blank line)
|
|
97
|
+
const updated = content.replace(/\n?\[mcp_servers\.jinn\]\ncommand = "jinn-mcp"\n?/g, '\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
|
|
98
|
+
writeFileSync(filePath, updated, 'utf-8');
|
|
99
|
+
return { ok: true, detail: `Removed MCP entry from ${filePath}` };
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Helpers — Skill block (delimited append for VS Code, Gemini, Codex)
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
const BLOCK_START = '<!-- jinn-operator-start -->';
|
|
105
|
+
const BLOCK_END = '<!-- jinn-operator-end -->';
|
|
106
|
+
function hasSkillBlock(filePath) {
|
|
107
|
+
if (!existsSync(filePath))
|
|
108
|
+
return false;
|
|
109
|
+
return readFileSync(filePath, 'utf-8').includes(BLOCK_START);
|
|
110
|
+
}
|
|
111
|
+
function upsertSkillBlock(filePath, content) {
|
|
112
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
113
|
+
const block = `${BLOCK_START}\n${content}\n${BLOCK_END}\n`;
|
|
114
|
+
if (hasSkillBlock(filePath)) {
|
|
115
|
+
// Replace existing block so `jinn plugin install` propagates updates
|
|
116
|
+
const existing = readFileSync(filePath, 'utf-8');
|
|
117
|
+
const re = new RegExp(BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
|
|
118
|
+
'[\\s\\S]*?' +
|
|
119
|
+
BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
|
|
120
|
+
'\\n?');
|
|
121
|
+
writeFileSync(filePath, existing.replace(re, block), 'utf-8');
|
|
122
|
+
return { ok: true, detail: `Updated skill block in ${filePath}` };
|
|
123
|
+
}
|
|
124
|
+
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
|
|
125
|
+
const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n\n' : existing.length > 0 ? '\n' : '';
|
|
126
|
+
writeFileSync(filePath, existing + separator + block, 'utf-8');
|
|
127
|
+
return { ok: true, detail: `Appended skill block to ${filePath}` };
|
|
128
|
+
}
|
|
129
|
+
function removeSkillBlock(filePath) {
|
|
130
|
+
if (!existsSync(filePath)) {
|
|
131
|
+
return { ok: true, detail: 'File does not exist' };
|
|
132
|
+
}
|
|
133
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
134
|
+
if (!content.includes(BLOCK_START)) {
|
|
135
|
+
return { ok: true, detail: 'Skill block not present' };
|
|
136
|
+
}
|
|
137
|
+
const regex = new RegExp(`\\n?${BLOCK_START}[\\s\\S]*?${BLOCK_END}\\n?`, 'g');
|
|
138
|
+
const updated = content.replace(regex, '\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
139
|
+
writeFileSync(filePath, updated, 'utf-8');
|
|
140
|
+
return { ok: true, detail: `Removed skill block from ${filePath}` };
|
|
141
|
+
}
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Helpers — Claude skill directory (copy/delete)
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
function hasClaudeSkill(targetDir) {
|
|
146
|
+
return existsSync(join(targetDir, 'SKILL.md'));
|
|
147
|
+
}
|
|
148
|
+
function installClaudeSkill(targetDir) {
|
|
149
|
+
const verb = hasClaudeSkill(targetDir) ? 'Updated' : 'Copied';
|
|
150
|
+
mkdirSync(targetDir, { recursive: true });
|
|
151
|
+
copyFileSync(join(SKILL_DIR, 'SKILL.md'), join(targetDir, 'SKILL.md'));
|
|
152
|
+
return { ok: true, detail: `${verb} skill at ${targetDir}` };
|
|
153
|
+
}
|
|
154
|
+
function removeClaudeSkill(targetDir) {
|
|
155
|
+
if (!hasClaudeSkill(targetDir)) {
|
|
156
|
+
return { ok: true, detail: 'Skill not installed' };
|
|
157
|
+
}
|
|
158
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
159
|
+
return { ok: true, detail: `Removed skill from ${targetDir}` };
|
|
160
|
+
}
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Helpers — Cursor rule file
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
function hasCursorRule(targetPath) {
|
|
165
|
+
return existsSync(targetPath);
|
|
166
|
+
}
|
|
167
|
+
function installCursorRule(targetPath, content) {
|
|
168
|
+
const verb = hasCursorRule(targetPath) ? 'Updated' : 'Wrote';
|
|
169
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
170
|
+
writeFileSync(targetPath, content, 'utf-8');
|
|
171
|
+
return { ok: true, detail: `${verb} rule at ${targetPath}` };
|
|
172
|
+
}
|
|
173
|
+
function removeCursorRule(targetPath) {
|
|
174
|
+
if (!hasCursorRule(targetPath)) {
|
|
175
|
+
return { ok: true, detail: 'Rule not installed' };
|
|
176
|
+
}
|
|
177
|
+
rmSync(targetPath, { force: true });
|
|
178
|
+
return { ok: true, detail: `Removed rule from ${targetPath}` };
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Helpers — command detection
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
function commandExists(cmd) {
|
|
184
|
+
try {
|
|
185
|
+
execSync(`which ${cmd}`, { stdio: 'ignore' });
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Platform paths
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
function claudeDesktopConfigDir() {
|
|
196
|
+
if (platform() === 'darwin') {
|
|
197
|
+
return join(homedir(), 'Library', 'Application Support', 'Claude');
|
|
198
|
+
}
|
|
199
|
+
return join(homedir(), '.config', 'claude-desktop');
|
|
200
|
+
}
|
|
201
|
+
function claudeSkillDir(scope) {
|
|
202
|
+
if (scope === 'user')
|
|
203
|
+
return join(homedir(), '.claude', 'skills', 'jinn-operator');
|
|
204
|
+
return join(process.cwd(), '.claude', 'skills', 'jinn-operator');
|
|
205
|
+
}
|
|
206
|
+
const TARGETS = [
|
|
207
|
+
// ---- claude-code ----
|
|
208
|
+
{
|
|
209
|
+
id: 'claude-code',
|
|
210
|
+
name: 'Claude Code',
|
|
211
|
+
detect: () => commandExists('claude'),
|
|
212
|
+
isMcpConfigured(scope) {
|
|
213
|
+
// We can't easily check this without parsing claude's internal config.
|
|
214
|
+
// Attempt to list MCP servers and check for jinn.
|
|
215
|
+
try {
|
|
216
|
+
const out = execSync(`claude mcp list --scope ${scope}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
217
|
+
return out.includes('jinn');
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
isSkillConfigured(scope) {
|
|
224
|
+
return hasClaudeSkill(claudeSkillDir(scope));
|
|
225
|
+
},
|
|
226
|
+
async installMcp(scope) {
|
|
227
|
+
try {
|
|
228
|
+
const out = execSync(`claude mcp list --scope ${scope}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
229
|
+
if (out.includes('jinn')) {
|
|
230
|
+
return { ok: true, detail: 'Already configured via claude CLI' };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch { /* proceed */ }
|
|
234
|
+
try {
|
|
235
|
+
execSync(`claude mcp add --scope ${scope} jinn -- jinn-mcp`, { stdio: 'ignore' });
|
|
236
|
+
return { ok: true, detail: `Added jinn MCP via claude CLI (scope: ${scope})` };
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
return { ok: false, detail: `Failed to add MCP: ${err instanceof Error ? err.message : String(err)}` };
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
async installSkill(scope) {
|
|
243
|
+
return installClaudeSkill(claudeSkillDir(scope));
|
|
244
|
+
},
|
|
245
|
+
async removeMcp(scope) {
|
|
246
|
+
try {
|
|
247
|
+
execSync(`claude mcp remove --scope ${scope} jinn`, { stdio: 'ignore' });
|
|
248
|
+
return { ok: true, detail: `Removed jinn MCP via claude CLI (scope: ${scope})` };
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
return { ok: false, detail: `Failed to remove MCP: ${err instanceof Error ? err.message : String(err)}` };
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
async removeSkill(scope) {
|
|
255
|
+
return removeClaudeSkill(claudeSkillDir(scope));
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
// ---- claude-desktop ----
|
|
259
|
+
{
|
|
260
|
+
id: 'claude-desktop',
|
|
261
|
+
name: 'Claude Desktop',
|
|
262
|
+
detect: () => existsSync(claudeDesktopConfigDir()),
|
|
263
|
+
isMcpConfigured(_scope) {
|
|
264
|
+
const cfgPath = join(claudeDesktopConfigDir(), 'claude_desktop_config.json');
|
|
265
|
+
return hasJsonMcpServer(cfgPath, 'mcpServers');
|
|
266
|
+
},
|
|
267
|
+
isSkillConfigured(scope) {
|
|
268
|
+
// Claude Desktop uses same skill directory as Claude Code (user scope always)
|
|
269
|
+
return hasClaudeSkill(claudeSkillDir(scope === 'project' ? 'user' : scope));
|
|
270
|
+
},
|
|
271
|
+
async installMcp(_scope) {
|
|
272
|
+
const cfgPath = join(claudeDesktopConfigDir(), 'claude_desktop_config.json');
|
|
273
|
+
return upsertJsonMcpServer(cfgPath, 'mcpServers');
|
|
274
|
+
},
|
|
275
|
+
async installSkill(scope) {
|
|
276
|
+
// Claude Desktop always uses user scope for skills
|
|
277
|
+
return installClaudeSkill(claudeSkillDir(scope === 'project' ? 'user' : scope));
|
|
278
|
+
},
|
|
279
|
+
async removeMcp(_scope) {
|
|
280
|
+
const cfgPath = join(claudeDesktopConfigDir(), 'claude_desktop_config.json');
|
|
281
|
+
return removeJsonMcpServer(cfgPath, 'mcpServers');
|
|
282
|
+
},
|
|
283
|
+
async removeSkill(scope) {
|
|
284
|
+
return removeClaudeSkill(claudeSkillDir(scope === 'project' ? 'user' : scope));
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
// ---- cursor ----
|
|
288
|
+
{
|
|
289
|
+
id: 'cursor',
|
|
290
|
+
name: 'Cursor',
|
|
291
|
+
detect: () => existsSync(join(homedir(), '.cursor')),
|
|
292
|
+
isMcpConfigured(scope) {
|
|
293
|
+
const cfgPath = scope === 'user'
|
|
294
|
+
? join(homedir(), '.cursor', 'mcp.json')
|
|
295
|
+
: join(process.cwd(), '.cursor', 'mcp.json');
|
|
296
|
+
return hasJsonMcpServer(cfgPath, 'mcpServers');
|
|
297
|
+
},
|
|
298
|
+
isSkillConfigured(scope) {
|
|
299
|
+
const rulePath = scope === 'user'
|
|
300
|
+
? join(homedir(), '.cursor', 'rules', 'jinn.md')
|
|
301
|
+
: join(process.cwd(), '.cursor', 'rules', 'jinn.md');
|
|
302
|
+
return hasCursorRule(rulePath);
|
|
303
|
+
},
|
|
304
|
+
async installMcp(scope) {
|
|
305
|
+
const cfgPath = scope === 'user'
|
|
306
|
+
? join(homedir(), '.cursor', 'mcp.json')
|
|
307
|
+
: join(process.cwd(), '.cursor', 'mcp.json');
|
|
308
|
+
return upsertJsonMcpServer(cfgPath, 'mcpServers');
|
|
309
|
+
},
|
|
310
|
+
async installSkill(scope, skillContent) {
|
|
311
|
+
const rulePath = scope === 'user'
|
|
312
|
+
? join(homedir(), '.cursor', 'rules', 'jinn.md')
|
|
313
|
+
: join(process.cwd(), '.cursor', 'rules', 'jinn.md');
|
|
314
|
+
return installCursorRule(rulePath, skillContent);
|
|
315
|
+
},
|
|
316
|
+
async removeMcp(scope) {
|
|
317
|
+
const cfgPath = scope === 'user'
|
|
318
|
+
? join(homedir(), '.cursor', 'mcp.json')
|
|
319
|
+
: join(process.cwd(), '.cursor', 'mcp.json');
|
|
320
|
+
return removeJsonMcpServer(cfgPath, 'mcpServers');
|
|
321
|
+
},
|
|
322
|
+
async removeSkill(scope) {
|
|
323
|
+
const rulePath = scope === 'user'
|
|
324
|
+
? join(homedir(), '.cursor', 'rules', 'jinn.md')
|
|
325
|
+
: join(process.cwd(), '.cursor', 'rules', 'jinn.md');
|
|
326
|
+
return removeCursorRule(rulePath);
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
// ---- vscode ----
|
|
330
|
+
{
|
|
331
|
+
id: 'vscode',
|
|
332
|
+
name: 'VS Code',
|
|
333
|
+
detect: () => commandExists('code'),
|
|
334
|
+
isMcpConfigured(scope) {
|
|
335
|
+
if (scope === 'user')
|
|
336
|
+
return false; // VS Code MCP is project-scoped only via .vscode/mcp.json
|
|
337
|
+
const cfgPath = join(process.cwd(), '.vscode', 'mcp.json');
|
|
338
|
+
return hasJsonMcpServer(cfgPath, 'servers');
|
|
339
|
+
},
|
|
340
|
+
isSkillConfigured(scope) {
|
|
341
|
+
if (scope === 'user')
|
|
342
|
+
return false;
|
|
343
|
+
const instrPath = join(process.cwd(), '.github', 'copilot-instructions.md');
|
|
344
|
+
return hasSkillBlock(instrPath);
|
|
345
|
+
},
|
|
346
|
+
async installMcp(scope) {
|
|
347
|
+
if (scope === 'user') {
|
|
348
|
+
return { ok: true, detail: 'VS Code MCP requires project scope (--scope project)' };
|
|
349
|
+
}
|
|
350
|
+
const cfgPath = join(process.cwd(), '.vscode', 'mcp.json');
|
|
351
|
+
return upsertJsonMcpServer(cfgPath, 'servers');
|
|
352
|
+
},
|
|
353
|
+
async installSkill(scope, skillContent) {
|
|
354
|
+
if (scope === 'user') {
|
|
355
|
+
return { ok: true, detail: 'VS Code skill requires project scope (--scope project)' };
|
|
356
|
+
}
|
|
357
|
+
const instrPath = join(process.cwd(), '.github', 'copilot-instructions.md');
|
|
358
|
+
return upsertSkillBlock(instrPath, stripFrontmatter(skillContent));
|
|
359
|
+
},
|
|
360
|
+
async removeMcp(scope) {
|
|
361
|
+
if (scope === 'user') {
|
|
362
|
+
return { ok: true, detail: 'Not configured at user scope' };
|
|
363
|
+
}
|
|
364
|
+
const cfgPath = join(process.cwd(), '.vscode', 'mcp.json');
|
|
365
|
+
return removeJsonMcpServer(cfgPath, 'servers');
|
|
366
|
+
},
|
|
367
|
+
async removeSkill(scope) {
|
|
368
|
+
if (scope === 'user') {
|
|
369
|
+
return { ok: true, detail: 'Not configured at user scope' };
|
|
370
|
+
}
|
|
371
|
+
const instrPath = join(process.cwd(), '.github', 'copilot-instructions.md');
|
|
372
|
+
return removeSkillBlock(instrPath);
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
// ---- gemini-cli ----
|
|
376
|
+
{
|
|
377
|
+
id: 'gemini-cli',
|
|
378
|
+
name: 'Gemini CLI',
|
|
379
|
+
detect: () => existsSync(join(homedir(), '.gemini')),
|
|
380
|
+
isMcpConfigured(scope) {
|
|
381
|
+
const cfgPath = scope === 'user'
|
|
382
|
+
? join(homedir(), '.gemini', 'settings.json')
|
|
383
|
+
: join(process.cwd(), '.gemini', 'settings.json');
|
|
384
|
+
return hasJsonMcpServer(cfgPath, 'mcpServers');
|
|
385
|
+
},
|
|
386
|
+
isSkillConfigured(scope) {
|
|
387
|
+
const instrPath = scope === 'user'
|
|
388
|
+
? join(homedir(), '.gemini', 'GEMINI.md')
|
|
389
|
+
: join(process.cwd(), 'GEMINI.md');
|
|
390
|
+
return hasSkillBlock(instrPath);
|
|
391
|
+
},
|
|
392
|
+
async installMcp(scope) {
|
|
393
|
+
const cfgPath = scope === 'user'
|
|
394
|
+
? join(homedir(), '.gemini', 'settings.json')
|
|
395
|
+
: join(process.cwd(), '.gemini', 'settings.json');
|
|
396
|
+
return upsertJsonMcpServer(cfgPath, 'mcpServers');
|
|
397
|
+
},
|
|
398
|
+
async installSkill(scope, skillContent) {
|
|
399
|
+
const instrPath = scope === 'user'
|
|
400
|
+
? join(homedir(), '.gemini', 'GEMINI.md')
|
|
401
|
+
: join(process.cwd(), 'GEMINI.md');
|
|
402
|
+
return upsertSkillBlock(instrPath, stripFrontmatter(skillContent));
|
|
403
|
+
},
|
|
404
|
+
async removeMcp(scope) {
|
|
405
|
+
const cfgPath = scope === 'user'
|
|
406
|
+
? join(homedir(), '.gemini', 'settings.json')
|
|
407
|
+
: join(process.cwd(), '.gemini', 'settings.json');
|
|
408
|
+
return removeJsonMcpServer(cfgPath, 'mcpServers');
|
|
409
|
+
},
|
|
410
|
+
async removeSkill(scope) {
|
|
411
|
+
const instrPath = scope === 'user'
|
|
412
|
+
? join(homedir(), '.gemini', 'GEMINI.md')
|
|
413
|
+
: join(process.cwd(), 'GEMINI.md');
|
|
414
|
+
return removeSkillBlock(instrPath);
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
// ---- antigravity ----
|
|
418
|
+
{
|
|
419
|
+
id: 'antigravity',
|
|
420
|
+
name: 'Antigravity',
|
|
421
|
+
detect: () => existsSync(join(homedir(), '.gemini', 'antigravity')),
|
|
422
|
+
isMcpConfigured(_scope) {
|
|
423
|
+
const cfgPath = join(homedir(), '.gemini', 'antigravity', 'mcp_config.json');
|
|
424
|
+
return hasJsonMcpServer(cfgPath, 'mcpServers');
|
|
425
|
+
},
|
|
426
|
+
isSkillConfigured(_scope) {
|
|
427
|
+
const instrPath = join(homedir(), '.gemini', 'GEMINI.md');
|
|
428
|
+
return hasSkillBlock(instrPath);
|
|
429
|
+
},
|
|
430
|
+
async installMcp(_scope) {
|
|
431
|
+
const cfgPath = join(homedir(), '.gemini', 'antigravity', 'mcp_config.json');
|
|
432
|
+
return upsertJsonMcpServer(cfgPath, 'mcpServers');
|
|
433
|
+
},
|
|
434
|
+
async installSkill(_scope, skillContent) {
|
|
435
|
+
const instrPath = join(homedir(), '.gemini', 'GEMINI.md');
|
|
436
|
+
return upsertSkillBlock(instrPath, stripFrontmatter(skillContent));
|
|
437
|
+
},
|
|
438
|
+
async removeMcp(_scope) {
|
|
439
|
+
const cfgPath = join(homedir(), '.gemini', 'antigravity', 'mcp_config.json');
|
|
440
|
+
return removeJsonMcpServer(cfgPath, 'mcpServers');
|
|
441
|
+
},
|
|
442
|
+
async removeSkill(_scope) {
|
|
443
|
+
const instrPath = join(homedir(), '.gemini', 'GEMINI.md');
|
|
444
|
+
return removeSkillBlock(instrPath);
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
// ---- codex ----
|
|
448
|
+
{
|
|
449
|
+
id: 'codex',
|
|
450
|
+
name: 'Codex',
|
|
451
|
+
detect: () => existsSync(join(homedir(), '.codex')),
|
|
452
|
+
isMcpConfigured(scope) {
|
|
453
|
+
const cfgPath = scope === 'user'
|
|
454
|
+
? join(homedir(), '.codex', 'config.toml')
|
|
455
|
+
: join(process.cwd(), '.codex', 'config.toml');
|
|
456
|
+
return hasTomlMcpServer(cfgPath);
|
|
457
|
+
},
|
|
458
|
+
isSkillConfigured(scope) {
|
|
459
|
+
const instrPath = scope === 'user'
|
|
460
|
+
? join(homedir(), '.codex', 'AGENTS.md')
|
|
461
|
+
: join(process.cwd(), 'AGENTS.md');
|
|
462
|
+
return hasSkillBlock(instrPath);
|
|
463
|
+
},
|
|
464
|
+
async installMcp(scope) {
|
|
465
|
+
const cfgPath = scope === 'user'
|
|
466
|
+
? join(homedir(), '.codex', 'config.toml')
|
|
467
|
+
: join(process.cwd(), '.codex', 'config.toml');
|
|
468
|
+
return upsertTomlMcpServer(cfgPath);
|
|
469
|
+
},
|
|
470
|
+
async installSkill(scope, skillContent) {
|
|
471
|
+
const instrPath = scope === 'user'
|
|
472
|
+
? join(homedir(), '.codex', 'AGENTS.md')
|
|
473
|
+
: join(process.cwd(), 'AGENTS.md');
|
|
474
|
+
return upsertSkillBlock(instrPath, stripFrontmatter(skillContent));
|
|
475
|
+
},
|
|
476
|
+
async removeMcp(scope) {
|
|
477
|
+
const cfgPath = scope === 'user'
|
|
478
|
+
? join(homedir(), '.codex', 'config.toml')
|
|
479
|
+
: join(process.cwd(), '.codex', 'config.toml');
|
|
480
|
+
return removeTomlMcpServer(cfgPath);
|
|
481
|
+
},
|
|
482
|
+
async removeSkill(scope) {
|
|
483
|
+
const instrPath = scope === 'user'
|
|
484
|
+
? join(homedir(), '.codex', 'AGENTS.md')
|
|
485
|
+
: join(process.cwd(), 'AGENTS.md');
|
|
486
|
+
return removeSkillBlock(instrPath);
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
];
|
|
490
|
+
async function runInstall(ctx, rest) {
|
|
491
|
+
let parsed;
|
|
492
|
+
try {
|
|
493
|
+
parsed = parseArgs({
|
|
494
|
+
args: rest,
|
|
495
|
+
options: {
|
|
496
|
+
scope: { type: 'string', default: 'user' },
|
|
497
|
+
target: { type: 'string' },
|
|
498
|
+
json: { type: 'boolean', default: false },
|
|
499
|
+
human: { type: 'boolean', default: false },
|
|
500
|
+
},
|
|
501
|
+
allowPositionals: false,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
emitEnvelope({
|
|
506
|
+
code: 'invalid_invocation',
|
|
507
|
+
message: err instanceof Error ? err.message : String(err),
|
|
508
|
+
exampleCli: 'jinn plugin install --scope user',
|
|
509
|
+
details: { field: 'flags' },
|
|
510
|
+
}, { writer: ctx.writer, exit: ctx.exit });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const scope = parsed.values.scope === 'project' ? 'project' : 'user';
|
|
514
|
+
const targetFilter = parsed.values.target;
|
|
515
|
+
let skillContent;
|
|
516
|
+
try {
|
|
517
|
+
skillContent = loadSkillContent();
|
|
518
|
+
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
emitEnvelope({
|
|
521
|
+
code: 'fatal',
|
|
522
|
+
message: `Failed to load skill content: ${err instanceof Error ? err.message : String(err)}`,
|
|
523
|
+
details: { skillDir: SKILL_DIR },
|
|
524
|
+
}, { writer: ctx.writer, exit: ctx.exit });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const targets = targetFilter
|
|
528
|
+
? TARGETS.filter((t) => t.id === targetFilter)
|
|
529
|
+
: TARGETS;
|
|
530
|
+
if (targetFilter && targets.length === 0) {
|
|
531
|
+
emitEnvelope({
|
|
532
|
+
code: 'invalid_invocation',
|
|
533
|
+
message: `Unknown target: ${targetFilter}`,
|
|
534
|
+
exampleCli: 'jinn plugin list',
|
|
535
|
+
details: { field: '--target', expected: TARGETS.map((t) => t.id).join('|') },
|
|
536
|
+
}, { writer: ctx.writer, exit: ctx.exit });
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const results = [];
|
|
540
|
+
for (const target of targets) {
|
|
541
|
+
if (!target.detect()) {
|
|
542
|
+
if (targetFilter) {
|
|
543
|
+
results.push({
|
|
544
|
+
target: target.id,
|
|
545
|
+
mcp: { status: 'not_found', detail: `${target.name} not detected` },
|
|
546
|
+
skill: { status: 'not_found', detail: `${target.name} not detected` },
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
let mcpResult;
|
|
552
|
+
if (target.isMcpConfigured(scope)) {
|
|
553
|
+
mcpResult = { status: 'skipped', detail: 'Already configured' };
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
const r = await target.installMcp(scope);
|
|
557
|
+
mcpResult = { status: r.ok ? 'configured' : 'error', detail: r.detail };
|
|
558
|
+
}
|
|
559
|
+
// Always run installSkill — the helpers handle both fresh installs and
|
|
560
|
+
// updates (replacing existing content), so skill changes propagate when
|
|
561
|
+
// the package is upgraded and `jinn plugin install` is re-run.
|
|
562
|
+
const sr = await target.installSkill(scope, skillContent);
|
|
563
|
+
const skillResult = { status: sr.ok ? 'configured' : 'error', detail: sr.detail };
|
|
564
|
+
results.push({ target: target.id, mcp: mcpResult, skill: skillResult });
|
|
565
|
+
}
|
|
566
|
+
emitResult({
|
|
567
|
+
schemaVersion: 1,
|
|
568
|
+
generatedAt: new Date().toISOString(),
|
|
569
|
+
verb: 'plugin install',
|
|
570
|
+
results,
|
|
571
|
+
}, (v) => {
|
|
572
|
+
const data = v;
|
|
573
|
+
if (data.results.length === 0)
|
|
574
|
+
return 'No targets detected.';
|
|
575
|
+
const maxLen = Math.max(...data.results.map((r) => r.target.length));
|
|
576
|
+
return data.results
|
|
577
|
+
.map((r) => `${r.target.padEnd(maxLen + 2)}MCP: ${r.mcp.status.padEnd(12)}Skill: ${r.skill.status}`)
|
|
578
|
+
.join('\n');
|
|
579
|
+
}, {
|
|
580
|
+
json: Boolean(parsed.values.json),
|
|
581
|
+
human: Boolean(parsed.values.human),
|
|
582
|
+
writer: ctx.writer,
|
|
583
|
+
stdoutIsTty: ctx.stdoutIsTty,
|
|
584
|
+
noColor: Boolean(ctx.env['NO_COLOR']),
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
// Subverb: remove
|
|
589
|
+
// ---------------------------------------------------------------------------
|
|
590
|
+
async function runRemove(ctx, rest) {
|
|
591
|
+
let parsed;
|
|
592
|
+
try {
|
|
593
|
+
parsed = parseArgs({
|
|
594
|
+
args: rest,
|
|
595
|
+
options: {
|
|
596
|
+
scope: { type: 'string', default: 'user' },
|
|
597
|
+
target: { type: 'string' },
|
|
598
|
+
json: { type: 'boolean', default: false },
|
|
599
|
+
human: { type: 'boolean', default: false },
|
|
600
|
+
},
|
|
601
|
+
allowPositionals: false,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
emitEnvelope({
|
|
606
|
+
code: 'invalid_invocation',
|
|
607
|
+
message: err instanceof Error ? err.message : String(err),
|
|
608
|
+
exampleCli: 'jinn plugin remove --target claude-code',
|
|
609
|
+
details: { field: 'flags' },
|
|
610
|
+
}, { writer: ctx.writer, exit: ctx.exit });
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const scope = parsed.values.scope === 'project' ? 'project' : 'user';
|
|
614
|
+
const targetFilter = parsed.values.target;
|
|
615
|
+
const targets = targetFilter
|
|
616
|
+
? TARGETS.filter((t) => t.id === targetFilter)
|
|
617
|
+
: TARGETS;
|
|
618
|
+
if (targetFilter && targets.length === 0) {
|
|
619
|
+
emitEnvelope({
|
|
620
|
+
code: 'invalid_invocation',
|
|
621
|
+
message: `Unknown target: ${targetFilter}`,
|
|
622
|
+
exampleCli: 'jinn plugin list',
|
|
623
|
+
details: { field: '--target', expected: TARGETS.map((t) => t.id).join('|') },
|
|
624
|
+
}, { writer: ctx.writer, exit: ctx.exit });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const results = [];
|
|
628
|
+
for (const target of targets) {
|
|
629
|
+
if (!target.detect()) {
|
|
630
|
+
if (targetFilter) {
|
|
631
|
+
results.push({
|
|
632
|
+
target: target.id,
|
|
633
|
+
mcp: { status: 'not_found', detail: `${target.name} not detected` },
|
|
634
|
+
skill: { status: 'not_found', detail: `${target.name} not detected` },
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
let mcpResult;
|
|
640
|
+
if (!target.isMcpConfigured(scope)) {
|
|
641
|
+
mcpResult = { status: 'skipped', detail: 'Not configured' };
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
const r = await target.removeMcp(scope);
|
|
645
|
+
mcpResult = { status: r.ok ? 'removed' : 'error', detail: r.detail };
|
|
646
|
+
}
|
|
647
|
+
let skillResult;
|
|
648
|
+
if (!target.isSkillConfigured(scope)) {
|
|
649
|
+
skillResult = { status: 'skipped', detail: 'Not configured' };
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
const r = await target.removeSkill(scope);
|
|
653
|
+
skillResult = { status: r.ok ? 'removed' : 'error', detail: r.detail };
|
|
654
|
+
}
|
|
655
|
+
results.push({ target: target.id, mcp: mcpResult, skill: skillResult });
|
|
656
|
+
}
|
|
657
|
+
emitResult({
|
|
658
|
+
schemaVersion: 1,
|
|
659
|
+
generatedAt: new Date().toISOString(),
|
|
660
|
+
verb: 'plugin remove',
|
|
661
|
+
results,
|
|
662
|
+
}, (v) => {
|
|
663
|
+
const data = v;
|
|
664
|
+
if (data.results.length === 0)
|
|
665
|
+
return 'No targets detected.';
|
|
666
|
+
const maxLen = Math.max(...data.results.map((r) => r.target.length));
|
|
667
|
+
return data.results
|
|
668
|
+
.map((r) => `${r.target.padEnd(maxLen + 2)}MCP: ${r.mcp.status.padEnd(12)}Skill: ${r.skill.status}`)
|
|
669
|
+
.join('\n');
|
|
670
|
+
}, {
|
|
671
|
+
json: Boolean(parsed.values.json),
|
|
672
|
+
human: Boolean(parsed.values.human),
|
|
673
|
+
writer: ctx.writer,
|
|
674
|
+
stdoutIsTty: ctx.stdoutIsTty,
|
|
675
|
+
noColor: Boolean(ctx.env['NO_COLOR']),
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
async function runList(ctx, rest) {
|
|
679
|
+
let parsed;
|
|
680
|
+
try {
|
|
681
|
+
parsed = parseArgs({
|
|
682
|
+
args: rest,
|
|
683
|
+
options: {
|
|
684
|
+
scope: { type: 'string', default: 'user' },
|
|
685
|
+
json: { type: 'boolean', default: false },
|
|
686
|
+
human: { type: 'boolean', default: false },
|
|
687
|
+
},
|
|
688
|
+
allowPositionals: false,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
emitEnvelope({
|
|
693
|
+
code: 'invalid_invocation',
|
|
694
|
+
message: err instanceof Error ? err.message : String(err),
|
|
695
|
+
exampleCli: 'jinn plugin list',
|
|
696
|
+
details: { field: 'flags' },
|
|
697
|
+
}, { writer: ctx.writer, exit: ctx.exit });
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const scope = parsed.values.scope === 'project' ? 'project' : 'user';
|
|
701
|
+
const targets = TARGETS.map((t) => {
|
|
702
|
+
const detected = t.detect();
|
|
703
|
+
return {
|
|
704
|
+
id: t.id,
|
|
705
|
+
name: t.name,
|
|
706
|
+
detected,
|
|
707
|
+
mcpConfigured: detected ? t.isMcpConfigured(scope) : false,
|
|
708
|
+
skillConfigured: detected ? t.isSkillConfigured(scope) : false,
|
|
709
|
+
};
|
|
710
|
+
});
|
|
711
|
+
emitResult({
|
|
712
|
+
schemaVersion: 1,
|
|
713
|
+
generatedAt: new Date().toISOString(),
|
|
714
|
+
verb: 'plugin list',
|
|
715
|
+
targets,
|
|
716
|
+
}, (v) => {
|
|
717
|
+
const data = v;
|
|
718
|
+
const maxLen = Math.max(...data.targets.map((t) => t.id.length));
|
|
719
|
+
return data.targets
|
|
720
|
+
.map((t) => {
|
|
721
|
+
if (!t.detected)
|
|
722
|
+
return `${t.id.padEnd(maxLen + 2)}not found`;
|
|
723
|
+
return `${t.id.padEnd(maxLen + 2)}detected MCP: ${t.mcpConfigured ? 'yes' : 'no'} Skill: ${t.skillConfigured ? 'yes' : 'no'}`;
|
|
724
|
+
})
|
|
725
|
+
.join('\n');
|
|
726
|
+
}, {
|
|
727
|
+
json: Boolean(parsed.values.json),
|
|
728
|
+
human: Boolean(parsed.values.human),
|
|
729
|
+
writer: ctx.writer,
|
|
730
|
+
stdoutIsTty: ctx.stdoutIsTty,
|
|
731
|
+
noColor: Boolean(ctx.env['NO_COLOR']),
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
// ---------------------------------------------------------------------------
|
|
735
|
+
// Verb dispatcher
|
|
736
|
+
// ---------------------------------------------------------------------------
|
|
737
|
+
async function run(ctx) {
|
|
738
|
+
const [subverb, ...rest] = ctx.argv;
|
|
739
|
+
if (!subverb) {
|
|
740
|
+
emitEnvelope({
|
|
741
|
+
code: 'invalid_invocation',
|
|
742
|
+
message: 'jinn plugin requires a subverb: install, remove, list',
|
|
743
|
+
exampleCli: 'jinn plugin install',
|
|
744
|
+
details: { field: 'subverb', expected: 'install|remove|list' },
|
|
745
|
+
}, { writer: ctx.writer, exit: ctx.exit });
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
switch (subverb) {
|
|
749
|
+
case 'install':
|
|
750
|
+
return runInstall(ctx, rest);
|
|
751
|
+
case 'remove':
|
|
752
|
+
return runRemove(ctx, rest);
|
|
753
|
+
case 'list':
|
|
754
|
+
return runList(ctx, rest);
|
|
755
|
+
default:
|
|
756
|
+
emitEnvelope({
|
|
757
|
+
code: 'invalid_invocation',
|
|
758
|
+
message: `Unknown plugin subverb: ${subverb}`,
|
|
759
|
+
exampleCli: 'jinn plugin install',
|
|
760
|
+
details: { field: 'subverb', expected: 'install|remove|list' },
|
|
761
|
+
}, { writer: ctx.writer, exit: ctx.exit });
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const command = {
|
|
765
|
+
name: 'plugin',
|
|
766
|
+
summary: 'Configure AI tools to use Jinn MCP server and operator skill',
|
|
767
|
+
helpText: `Usage: jinn plugin <install|remove|list> [options]
|
|
768
|
+
|
|
769
|
+
Subcommands:
|
|
770
|
+
install Configure detected AI tools with Jinn MCP server and operator skill
|
|
771
|
+
remove Remove Jinn configuration from AI tools
|
|
772
|
+
list Show detected AI tools and their configuration status
|
|
773
|
+
|
|
774
|
+
Options:
|
|
775
|
+
--scope <user|project> Install scope (default: user)
|
|
776
|
+
--target <id> Configure only this target
|
|
777
|
+
--json JSON output (default)
|
|
778
|
+
--human Human-readable output
|
|
779
|
+
|
|
780
|
+
Supported targets:
|
|
781
|
+
claude-code Claude Code CLI
|
|
782
|
+
claude-desktop Claude Desktop app
|
|
783
|
+
cursor Cursor editor
|
|
784
|
+
vscode VS Code (Copilot)
|
|
785
|
+
gemini-cli Gemini CLI
|
|
786
|
+
antigravity Antigravity (Gemini)
|
|
787
|
+
codex OpenAI Codex CLI
|
|
788
|
+
|
|
789
|
+
Examples:
|
|
790
|
+
jinn plugin list --human
|
|
791
|
+
jinn plugin install --human
|
|
792
|
+
jinn plugin install --target claude-code --human
|
|
793
|
+
jinn plugin install --scope project --target cursor
|
|
794
|
+
jinn plugin remove --target claude-code
|
|
795
|
+
`,
|
|
796
|
+
run,
|
|
797
|
+
};
|
|
798
|
+
export default command;
|
|
799
|
+
//# sourceMappingURL=plugin-install.js.map
|