chain-insights 0.46.6 → 0.47.0

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/bin/install.cjs CHANGED
@@ -33,7 +33,7 @@ if (!hasClaude && !hasCodex && !hasHermes && !hasClaudeDesktop && !hasLocal) {
33
33
  ` ${cyan}--claude-desktop${reset} Register MCP in Claude Desktop (draws the money-flow views)`
34
34
  )
35
35
  console.log(
36
- ` ${cyan}--codex${reset} Install Codex skills globally to ~/.codex/skills/ and register MCP`
36
+ ` ${cyan}--codex${reset} Install Codex skills to ~/.codex/skills/ and the Chain Insights Codex plugin (draws views)`
37
37
  )
38
38
  console.log(
39
39
  ` ${cyan}--hermes${reset} Install Hermes skills globally to ~/.hermes/skills/chain-insights/ and register MCP`
@@ -170,36 +170,128 @@ function tomlQuoted(value) {
170
170
  return JSON.stringify(value)
171
171
  }
172
172
 
173
- function installCodexMcp(configFile, proxyPath) {
174
- fs.mkdirSync(path.dirname(configFile), { recursive: true })
175
- let content = fs.existsSync(configFile) ? fs.readFileSync(configFile, 'utf8') : ''
176
- const block = [
177
- '[mcp_servers.chain-insights]',
178
- 'command = "node"',
179
- `args = [${tomlQuoted(proxyPath)}]`,
180
- '',
181
- ].join('\n')
182
-
183
- const heading = '[mcp_servers.chain-insights]'
184
- const start = content.indexOf(heading)
185
- if (start >= 0) {
186
- let end = content.length
187
- const rest = content.slice(start + heading.length)
188
- const nextSection = rest.search(/\n\[/)
189
- if (nextSection >= 0) end = start + heading.length + nextSection + 1
190
- content = `${content.slice(0, start)}${block}${content.slice(end)}`
191
- } else {
192
- const separator = content.endsWith('\n') || content.length === 0 ? '' : '\n'
193
- content = `${content}${separator}\n${block}`
173
+ // Codex draws an MCP app view only for a server that a Codex plugin provides:
174
+ // the desktop app needs both the view's ui:// address and a plugin id on the
175
+ // tool call. A plain [mcp_servers] entry has no plugin id, so its graph_query
176
+ // answers stay text. setup codex therefore installs a local one-plugin
177
+ // marketplace and removes the plain entry earlier versions wrote.
178
+ const CODEX_MARKETPLACE = 'chain-insights'
179
+ const CODEX_PLUGIN = 'chain-insights'
180
+ const CODEX_PLUGIN_REF = `${CODEX_PLUGIN}@${CODEX_MARKETPLACE}`
181
+ const LEGACY_CODEX_MCP_HEADING = '[mcp_servers.chain-insights]'
182
+
183
+ function packageVersion() {
184
+ return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version
185
+ }
186
+
187
+ function writeJson(file, value) {
188
+ fs.mkdirSync(path.dirname(file), { recursive: true })
189
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
190
+ }
191
+
192
+ function writeCodexMarketplace(root, proxyPath, version) {
193
+ writeJson(path.join(root, '.agents', 'plugins', 'marketplace.json'), {
194
+ name: CODEX_MARKETPLACE,
195
+ interface: { displayName: 'Chain Insights' },
196
+ plugins: [
197
+ {
198
+ name: CODEX_PLUGIN,
199
+ source: { source: 'local', path: `./plugins/${CODEX_PLUGIN}` },
200
+ policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },
201
+ category: 'Developer Tools',
202
+ },
203
+ ],
204
+ })
205
+ const pluginDir = path.join(root, 'plugins', CODEX_PLUGIN)
206
+ writeJson(path.join(pluginDir, '.codex-plugin', 'plugin.json'), {
207
+ name: CODEX_PLUGIN,
208
+ version,
209
+ description: 'Chain Insights graph queries, with money-flow, chart and table views.',
210
+ author: { name: 'Chain Insights' },
211
+ license: 'MIT',
212
+ mcpServers: './.mcp.json',
213
+ interface: { displayName: 'Chain Insights', shortDescription: 'Money flows and graph queries' },
214
+ })
215
+ // The installing Node's absolute path, as setup claude-desktop does: a
216
+ // desktop app does not inherit the shell's PATH.
217
+ writeJson(path.join(pluginDir, '.mcp.json'), {
218
+ mcpServers: { 'chain-insights': { command: process.execPath, args: [proxyPath] } },
219
+ })
220
+ }
221
+
222
+ // The [heading] section of a TOML file, from its heading to the next heading.
223
+ function tomlSectionRange(content, heading) {
224
+ const lines = content.split('\n')
225
+ const start = lines.findIndex((line) => line.trim() === heading)
226
+ if (start < 0) return null
227
+ let end = start + 1
228
+ while (end < lines.length && !/^\s*\[/.test(lines[end])) end++
229
+ return { lines, start, end }
230
+ }
231
+
232
+ function removeTomlSection(content, heading) {
233
+ const range = tomlSectionRange(content, heading)
234
+ if (!range) return content
235
+ const { lines, start, end } = range
236
+ return [...lines.slice(0, start), ...lines.slice(end)].join('\n').replace(/\n{3,}/g, '\n\n')
237
+ }
238
+
239
+ function setTomlSection(content, heading, body) {
240
+ const block = [heading, ...body, '']
241
+ const range = tomlSectionRange(content, heading)
242
+ if (range) {
243
+ const { lines, start, end } = range
244
+ return [...lines.slice(0, start), ...block, ...lines.slice(end)].join('\n')
194
245
  }
246
+ const separator = content.endsWith('\n') || content.length === 0 ? '' : '\n'
247
+ return `${content}${separator}\n${block.join('\n')}`
248
+ }
195
249
 
250
+ function installCodexPlugin(configFile, marketplaceRoot) {
251
+ fs.mkdirSync(path.dirname(configFile), { recursive: true })
252
+ let content = fs.existsSync(configFile) ? fs.readFileSync(configFile, 'utf8') : ''
253
+ content = removeTomlSection(content, LEGACY_CODEX_MCP_HEADING)
254
+ content = setTomlSection(content, `[marketplaces.${CODEX_MARKETPLACE}]`, [
255
+ 'source_type = "local"',
256
+ `source = ${tomlQuoted(marketplaceRoot)}`,
257
+ ])
258
+ content = setTomlSection(content, `[plugins.${tomlQuoted(CODEX_PLUGIN_REF)}]`, ['enabled = true'])
196
259
  fs.writeFileSync(configFile, content, 'utf8')
197
260
  }
198
261
 
262
+ // Codex copies a plugin into its own cache on `codex plugin add`. Without the
263
+ // codex command the config entries above are enough for the next start; with
264
+ // it, the cache is refreshed now, so an upgrade takes the new version.
265
+ function cacheCodexPlugin() {
266
+ try {
267
+ execFileSync('codex', ['plugin', 'remove', CODEX_PLUGIN_REF], {
268
+ stdio: 'ignore',
269
+ timeout: 30000,
270
+ })
271
+ } catch {
272
+ // Not installed yet, or no codex command: the add below decides.
273
+ }
274
+ try {
275
+ execFileSync('codex', ['plugin', 'add', CODEX_PLUGIN_REF], { stdio: 'ignore', timeout: 30000 })
276
+ return true
277
+ } catch {
278
+ return false
279
+ }
280
+ }
281
+
199
282
  if (hasCodex) {
200
283
  const codexConfig = path.join(homeDir, '.codex', 'config.toml')
201
- installCodexMcp(codexConfig, proxyBinPath)
202
- console.log(` ${cyan}Codex MCP:${reset} registered in ${codexConfig}`)
284
+ const marketplaceRoot = path.join(dataDir, 'codex-marketplace')
285
+ writeCodexMarketplace(marketplaceRoot, proxyBinPath, packageVersion())
286
+ installCodexPlugin(codexConfig, marketplaceRoot)
287
+ console.log(` ${cyan}Codex plugin:${reset} ${CODEX_PLUGIN_REF} registered in ${codexConfig}`)
288
+ if (cacheCodexPlugin()) {
289
+ console.log(` ${cyan}Codex plugin:${reset} installed; restart the Codex app to load it`)
290
+ } else {
291
+ console.log(
292
+ ` ${dim}Codex plugin:${reset} run once: codex plugin add ${CODEX_PLUGIN_REF}, then restart the Codex app`
293
+ )
294
+ }
203
295
  }
204
296
 
205
297
  function yamlQuoted(value) {
@@ -280,7 +372,13 @@ if (hasHermes) {
280
372
  // absolute path. Skills reach Claude Desktop through the plugin, not this file.
281
373
  function claudeDesktopConfigPath() {
282
374
  if (process.platform === 'darwin') {
283
- return path.join(homeDir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
375
+ return path.join(
376
+ homeDir,
377
+ 'Library',
378
+ 'Application Support',
379
+ 'Claude',
380
+ 'claude_desktop_config.json'
381
+ )
284
382
  }
285
383
  if (process.platform === 'win32') {
286
384
  const appData = process.env['APPDATA'] || path.join(homeDir, 'AppData', 'Roaming')
@@ -302,7 +400,9 @@ function installClaudeDesktopMcp(configFile, proxyPath) {
302
400
  throw new Error(`${configFile} is not valid JSON; fix it, then run the setup again`)
303
401
  }
304
402
  if (config === null || typeof config !== 'object' || Array.isArray(config)) {
305
- throw new Error(`${configFile} does not hold a JSON object; fix it, then run the setup again`)
403
+ throw new Error(
404
+ `${configFile} does not hold a JSON object; fix it, then run the setup again`
405
+ )
306
406
  }
307
407
  }
308
408
  fs.copyFileSync(configFile, `${configFile}.bak`)
@@ -322,10 +422,13 @@ function claudeDesktopRunning() {
322
422
  const { spawnSync } = require('child_process')
323
423
  try {
324
424
  if (process.platform === 'win32') {
325
- const out = spawnSync('tasklist', ['/FI', 'IMAGENAME eq Claude.exe', '/NH'], { encoding: 'utf8' })
425
+ const out = spawnSync('tasklist', ['/FI', 'IMAGENAME eq Claude.exe', '/NH'], {
426
+ encoding: 'utf8',
427
+ })
326
428
  return /claude\.exe/i.test(out.stdout || '')
327
429
  }
328
- const pgrepArgs = process.platform === 'darwin' ? ['-x', 'Claude'] : ['-f', '(^|/)claude-desktop( |$)']
430
+ const pgrepArgs =
431
+ process.platform === 'darwin' ? ['-x', 'Claude'] : ['-f', '(^|/)claude-desktop( |$)']
329
432
  return spawnSync('pgrep', pgrepArgs, { encoding: 'utf8' }).status === 0
330
433
  } catch {
331
434
  return false
@@ -341,8 +444,12 @@ function quitClaudeDesktopHint() {
341
444
  if (hasClaudeDesktop) {
342
445
  const desktopConfig = claudeDesktopConfigPath()
343
446
  if (claudeDesktopRunning()) {
344
- console.log(` ${bold}Claude Desktop is running.${reset} It can overwrite the new entry when it saves its own settings.`)
345
- console.log(` Quit it fully (${quitClaudeDesktopHint()}), run ${cyan}cia setup claude-desktop${reset} again, then start Claude Desktop.`)
447
+ console.log(
448
+ ` ${bold}Claude Desktop is running.${reset} It can overwrite the new entry when it saves its own settings.`
449
+ )
450
+ console.log(
451
+ ` Quit it fully (${quitClaudeDesktopHint()}), run ${cyan}cia setup claude-desktop${reset} again, then start Claude Desktop.`
452
+ )
346
453
  }
347
454
  try {
348
455
  installClaudeDesktopMcp(desktopConfig, proxyBinPath)
package/docs/mcp-proxy.md CHANGED
@@ -279,6 +279,18 @@ Desktop's `claude_desktop_config.json`, keeps every other entry, saves a
279
279
  `.bak` copy, and refuses a file that is not valid JSON. Restart Claude
280
280
  Desktop afterwards.
281
281
 
282
+ `cia setup codex` installs the Chain Insights Codex plugin: a local
283
+ one-plugin marketplace under `~/.chain-insights/codex-marketplace`, registered
284
+ in `~/.codex/config.toml` as `chain-insights@chain-insights`, with the skills
285
+ copied to `~/.codex/skills`. The Codex app draws the graph, chart and table
286
+ views only for a server that a plugin provides, so the plain
287
+ `[mcp_servers.chain-insights]` entry that earlier versions wrote is removed.
288
+ Every other entry in `config.toml` is kept. When the `codex` command is on
289
+ `PATH`, setup also runs `codex plugin add chain-insights@chain-insights`;
290
+ otherwise run it once yourself. Restart the Codex app afterwards. To remove
291
+ the plugin: `codex plugin remove chain-insights@chain-insights`, then
292
+ `codex plugin marketplace remove chain-insights`.
293
+
282
294
  Current MCP prompts exposed by the local proxy:
283
295
 
284
296
  - `meta-network-capabilities`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chain-insights",
3
- "version": "0.46.6",
3
+ "version": "0.47.0",
4
4
  "description": "AML investigation CLI and MCP proxy for blockchain risk screening and Chain Insights Graph access",
5
5
  "homepage": "https://chain-insights.ai",
6
6
  "repository": {