@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.
Files changed (101) hide show
  1. package/README.md +46 -9
  2. package/dist/adapters/mech/adapter.d.ts +1 -0
  3. package/dist/adapters/mech/adapter.js +35 -0
  4. package/dist/adapters/mech/adapter.js.map +1 -1
  5. package/dist/api/gather-status.js +1 -0
  6. package/dist/api/gather-status.js.map +1 -1
  7. package/dist/api/server.js +12 -0
  8. package/dist/api/server.js.map +1 -1
  9. package/dist/api/status-build.d.ts +1 -0
  10. package/dist/api/status-build.js.map +1 -1
  11. package/dist/api/status-rollup-build.d.ts +4 -0
  12. package/dist/api/status-rollup-build.js +4 -0
  13. package/dist/api/status-rollup-build.js.map +1 -1
  14. package/dist/bin/jinn-mcp.d.ts +14 -0
  15. package/dist/bin/jinn-mcp.js +19 -0
  16. package/dist/bin/jinn-mcp.js.map +1 -0
  17. package/dist/build-meta.json +1 -1
  18. package/dist/cli/commands/auth.d.ts +3 -0
  19. package/dist/cli/commands/auth.js +146 -0
  20. package/dist/cli/commands/auth.js.map +1 -0
  21. package/dist/cli/commands/bootstrap.js +1 -0
  22. package/dist/cli/commands/bootstrap.js.map +1 -1
  23. package/dist/cli/commands/doctor.js +43 -11
  24. package/dist/cli/commands/doctor.js.map +1 -1
  25. package/dist/cli/commands/fund-requirements.js +69 -1
  26. package/dist/cli/commands/fund-requirements.js.map +1 -1
  27. package/dist/cli/commands/history.js +1 -0
  28. package/dist/cli/commands/history.js.map +1 -1
  29. package/dist/cli/commands/init.js +31 -7
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/keys-backup.js +142 -10
  32. package/dist/cli/commands/keys-backup.js.map +1 -1
  33. package/dist/cli/commands/logs.js +28 -13
  34. package/dist/cli/commands/logs.js.map +1 -1
  35. package/dist/cli/commands/plugin-install.d.ts +3 -0
  36. package/dist/cli/commands/plugin-install.js +799 -0
  37. package/dist/cli/commands/plugin-install.js.map +1 -0
  38. package/dist/cli/commands/quickstart.d.ts +3 -0
  39. package/dist/cli/commands/quickstart.js +236 -0
  40. package/dist/cli/commands/quickstart.js.map +1 -0
  41. package/dist/cli/commands/run.js +6 -0
  42. package/dist/cli/commands/run.js.map +1 -1
  43. package/dist/cli/commands/stop.js +1 -0
  44. package/dist/cli/commands/stop.js.map +1 -1
  45. package/dist/cli/commands/submit-intent.js +11 -1
  46. package/dist/cli/commands/submit-intent.js.map +1 -1
  47. package/dist/cli/commands/update.d.ts +3 -0
  48. package/dist/cli/commands/update.js +154 -0
  49. package/dist/cli/commands/update.js.map +1 -0
  50. package/dist/cli/commands/version.js +15 -1
  51. package/dist/cli/commands/version.js.map +1 -1
  52. package/dist/cli/deployment-digest.js +20 -4
  53. package/dist/cli/deployment-digest.js.map +1 -1
  54. package/dist/cli/index.js +8 -0
  55. package/dist/cli/index.js.map +1 -1
  56. package/dist/cli/password.d.ts +15 -0
  57. package/dist/cli/password.js +29 -1
  58. package/dist/cli/password.js.map +1 -1
  59. package/dist/config.d.ts +8 -0
  60. package/dist/config.js +8 -0
  61. package/dist/config.js.map +1 -1
  62. package/dist/daemon/balance-topup-loop.d.ts +40 -0
  63. package/dist/daemon/balance-topup-loop.js +96 -0
  64. package/dist/daemon/balance-topup-loop.js.map +1 -0
  65. package/dist/daemon/daemon.d.ts +7 -0
  66. package/dist/daemon/daemon.js +12 -0
  67. package/dist/daemon/daemon.js.map +1 -1
  68. package/dist/dashboard/index.html +500 -0
  69. package/dist/earning/bootstrap.d.ts +2 -0
  70. package/dist/earning/bootstrap.js +68 -14
  71. package/dist/earning/bootstrap.js.map +1 -1
  72. package/dist/earning/contracts.d.ts +17 -0
  73. package/dist/earning/contracts.js +7 -1
  74. package/dist/earning/contracts.js.map +1 -1
  75. package/dist/earning/faucet.d.ts +15 -0
  76. package/dist/earning/faucet.js +64 -0
  77. package/dist/earning/faucet.js.map +1 -0
  78. package/dist/earning/store.d.ts +5 -0
  79. package/dist/earning/store.js +7 -3
  80. package/dist/earning/store.js.map +1 -1
  81. package/dist/errors/unauthorized-account.d.ts +10 -0
  82. package/dist/errors/unauthorized-account.js +14 -0
  83. package/dist/errors/unauthorized-account.js.map +1 -0
  84. package/dist/main.js +30 -1
  85. package/dist/main.js.map +1 -1
  86. package/dist/mcp/operator-server.d.ts +34 -0
  87. package/dist/mcp/operator-server.js +219 -0
  88. package/dist/mcp/operator-server.js.map +1 -0
  89. package/dist/operator-errors.js +11 -0
  90. package/dist/operator-errors.js.map +1 -1
  91. package/dist/preflight/claude-auth.d.ts +57 -0
  92. package/dist/preflight/claude-auth.js +153 -0
  93. package/dist/preflight/claude-auth.js.map +1 -0
  94. package/dist/runner/claude.js +15 -0
  95. package/dist/runner/claude.js.map +1 -1
  96. package/dist/store/store.js +5 -0
  97. package/dist/store/store.js.map +1 -1
  98. package/dist/tx-retry.js +11 -1
  99. package/dist/tx-retry.js.map +1 -1
  100. package/package.json +12 -3
  101. 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