@injectivelabs/ainj 0.0.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.
@@ -0,0 +1,85 @@
1
+ import { readState } from '../install/state.js';
2
+ import { execFileAsync } from './exec.js';
3
+
4
+ const DEFAULT_SKILL_TIMEOUT_MS = 60_000;
5
+ const DEFAULT_VERSION_TIMEOUT_MS = 5_000;
6
+
7
+ const HARNESS_BINS = {
8
+ claude: 'claude',
9
+ codex: 'codex',
10
+ };
11
+
12
+ function buildPrompt(skillName, params) {
13
+ if (typeof params === 'undefined' || params === null) {
14
+ return `/${skillName}`;
15
+ }
16
+ if (typeof params === 'string') {
17
+ return `/${skillName} ${params}`;
18
+ }
19
+ return `/${skillName} ${JSON.stringify(params)}`;
20
+ }
21
+
22
+ async function runWithHarness(skillName, params, opts, harness) {
23
+ const bin = HARNESS_BINS[harness];
24
+ if (!bin) {
25
+ throw new Error(
26
+ `Unknown harness '${harness}'. Supported harnesses: ${Object.keys(HARNESS_BINS).join(', ')}`,
27
+ );
28
+ }
29
+
30
+ const skillTimeout = opts?.timeout ?? DEFAULT_SKILL_TIMEOUT_MS;
31
+ const startTimestamp = Date.now();
32
+
33
+ let harnessVersion = 'unknown';
34
+ try {
35
+ const { stdout } = await execFileAsync(bin, ['--version'], {
36
+ timeout: DEFAULT_VERSION_TIMEOUT_MS,
37
+ });
38
+ harnessVersion = stdout.trim();
39
+ } catch {
40
+ // fallback to 'unknown'
41
+ }
42
+
43
+ const prompt = buildPrompt(skillName, params);
44
+ let stdout = '';
45
+ let stderr = '';
46
+ let exitCode = 0;
47
+
48
+ try {
49
+ const result = await execFileAsync(bin, ['-p', prompt, '--output-format', 'text'], {
50
+ timeout: skillTimeout,
51
+ });
52
+ stdout = result.stdout ?? '';
53
+ stderr = result.stderr ?? '';
54
+ } catch (err) {
55
+ if (err.killed) throw err;
56
+ stdout = err.stdout ?? '';
57
+ stderr = err.stderr ?? '';
58
+ exitCode = typeof err.code === 'number' ? err.code : 1;
59
+ }
60
+
61
+ const endTimestamp = Date.now();
62
+
63
+ return { stdout, stderr, exitCode, harness, harnessVersion, startTimestamp, endTimestamp };
64
+ }
65
+
66
+ async function run(skillName, params, opts) {
67
+ const globalState = readState('global', opts);
68
+ const localState = readState('local', opts);
69
+ const harness = globalState.defaultHarness ?? localState.defaultHarness;
70
+ if (!harness) {
71
+ throw new Error('No AI harness configured. Run `ainj install` to set one up.');
72
+ }
73
+ return runWithHarness(skillName, params, opts, harness);
74
+ }
75
+
76
+ async function runWithClaude(skillName, params, opts) {
77
+ return runWithHarness(skillName, params, opts, 'claude');
78
+ }
79
+
80
+ async function runWithCodex(skillName, params, opts) {
81
+ return runWithHarness(skillName, params, opts, 'codex');
82
+ }
83
+
84
+ export const skills = { run, runWithClaude, runWithCodex };
85
+ export default skills;
@@ -0,0 +1,11 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+
4
+ const DOCS_MCP_URL = 'https://docs.injective.network/mcp';
5
+
6
+ export async function createOutboundClient() {
7
+ const transport = new StreamableHTTPClientTransport(new URL(DOCS_MCP_URL));
8
+ const client = new Client({ name: 'ainj-docs', version: '0.1.0' });
9
+ await client.connect(transport);
10
+ return client;
11
+ }
@@ -0,0 +1,125 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createServer as createHttpServer } from 'node:http';
3
+ import { createOutboundClient } from './client.js';
4
+
5
+ export async function buildProxyServer() {
6
+ const remote = await createOutboundClient();
7
+ const { tools } = await remote.listTools();
8
+
9
+ const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
10
+ const { ListToolsRequestSchema, CallToolRequestSchema } = await import(
11
+ '@modelcontextprotocol/sdk/types.js'
12
+ );
13
+
14
+ function makeServer() {
15
+ const server = new Server(
16
+ { name: 'ainj-docs', version: '0.1.0' },
17
+ { capabilities: { tools: {} } },
18
+ );
19
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
20
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
21
+ return remote.callTool({ name: req.params.name, arguments: req.params.arguments ?? {} });
22
+ });
23
+ return server;
24
+ }
25
+
26
+ return { makeServer, remote };
27
+ }
28
+
29
+ function readJsonBody(req) {
30
+ return new Promise((resolve, reject) => {
31
+ let raw = '';
32
+ req.on('data', (chunk) => {
33
+ raw += chunk;
34
+ });
35
+ req.on('end', () => {
36
+ try {
37
+ resolve(JSON.parse(raw));
38
+ } catch {
39
+ resolve(undefined);
40
+ }
41
+ });
42
+ req.on('error', reject);
43
+ });
44
+ }
45
+
46
+ export function resolveHttpPort(port) {
47
+ return port ?? Number(process.env.AINJ_MCP_DOCS_PORT ?? 3002);
48
+ }
49
+
50
+ export async function startHttp(port) {
51
+ const effectivePort = resolveHttpPort(port);
52
+ const { makeServer, remote } = await buildProxyServer();
53
+
54
+ const { StreamableHTTPServerTransport } = await import(
55
+ '@modelcontextprotocol/sdk/server/streamableHttp.js'
56
+ );
57
+
58
+ const sessions = new Map();
59
+
60
+ const httpServer = createHttpServer(async (req, res) => {
61
+ try {
62
+ const path = req.url?.split('?')[0];
63
+
64
+ if (path !== '/mcp') {
65
+ res.writeHead(404).end();
66
+ return;
67
+ }
68
+
69
+ const sessionId = req.headers['mcp-session-id'];
70
+
71
+ if (sessionId) {
72
+ const session = sessions.get(sessionId);
73
+ if (!session) {
74
+ res.writeHead(404).end('Session not found');
75
+ return;
76
+ }
77
+ await session.transport.handleRequest(req, res);
78
+ } else if (req.method === 'POST') {
79
+ const parsedBody = await readJsonBody(req);
80
+ const newSessionId = randomUUID();
81
+ const transport = new StreamableHTTPServerTransport({
82
+ sessionIdGenerator: () => newSessionId,
83
+ });
84
+ const server = makeServer();
85
+
86
+ transport.onclose = () => sessions.delete(newSessionId);
87
+
88
+ await server.connect(transport);
89
+ sessions.set(newSessionId, { transport, server });
90
+
91
+ await transport.handleRequest(req, res, parsedBody);
92
+ } else {
93
+ res.writeHead(405).end('Method not allowed');
94
+ }
95
+ } catch (err) {
96
+ if (!res.headersSent) res.writeHead(500).end(String(err));
97
+ }
98
+ });
99
+
100
+ await new Promise((resolve, reject) => {
101
+ httpServer.listen(effectivePort, '127.0.0.1', resolve);
102
+ httpServer.on('error', reject);
103
+ });
104
+
105
+ const _close = httpServer.close.bind(httpServer);
106
+ httpServer.close = (cb) => {
107
+ _close((err) => {
108
+ remote
109
+ .close?.()
110
+ .catch(() => {})
111
+ .then(() => cb?.(err));
112
+ });
113
+ return httpServer;
114
+ };
115
+
116
+ return httpServer;
117
+ }
118
+
119
+ export async function startStdio() {
120
+ const { makeServer } = await buildProxyServer();
121
+ const server = makeServer();
122
+ const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
123
+ const transport = new StdioServerTransport();
124
+ await server.connect(transport);
125
+ }
@@ -0,0 +1,144 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { createServer as createHttpServer } from 'node:http';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ function resolveServerScript() {
7
+ return fileURLToPath(import.meta.resolve('@injective-agent/core/mcp'));
8
+ }
9
+
10
+ // Returns a child process with piped stdin/stdout.
11
+ // The caller (mcp-cli.js) is responsible for connecting process stdio to the child's pipes.
12
+ export function startStdio() {
13
+ const script = resolveServerScript();
14
+ return spawn(process.execPath, [script], { stdio: 'inherit' });
15
+ }
16
+
17
+ export function resolveHttpPort(port) {
18
+ return port ?? Number(process.env.AINJ_MCP_MAIN_PORT ?? 3001);
19
+ }
20
+
21
+ export async function startHttp(port) {
22
+ const effectivePort = resolveHttpPort(port);
23
+ const script = resolveServerScript();
24
+
25
+ // Dynamic imports, transitively available when @injective-agent/core is installed
26
+ const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
27
+ const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
28
+ const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
29
+ const { StreamableHTTPServerTransport } = await import(
30
+ '@modelcontextprotocol/sdk/server/streamableHttp.js'
31
+ );
32
+ const { ListToolsRequestSchema, CallToolRequestSchema } = await import(
33
+ '@modelcontextprotocol/sdk/types.js'
34
+ );
35
+
36
+ // Connect once to the upstream stdio server (shared across all HTTP sessions)
37
+ const upstreamClient = new Client({ name: 'ainj-main', version: '0.1.0' });
38
+ const upstreamTransport = new StdioClientTransport({
39
+ command: process.execPath,
40
+ args: [script],
41
+ env: { ...process.env },
42
+ });
43
+ await upstreamClient.connect(upstreamTransport);
44
+ const { tools } = await upstreamClient.listTools();
45
+
46
+ function makeMcpServer() {
47
+ const server = new Server(
48
+ { name: 'injective-agent', version: '0.1.0' },
49
+ { capabilities: { tools: {} } },
50
+ );
51
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
52
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
53
+ return upstreamClient.callTool({
54
+ name: req.params.name,
55
+ arguments: req.params.arguments ?? {},
56
+ });
57
+ });
58
+ return server;
59
+ }
60
+
61
+ // Read and JSON-parse the request body from a Node.js IncomingMessage.
62
+ // Needed because StreamableHTTPServerTransport expects the body pre-parsed
63
+ // when we construct the web Request from an already-read stream.
64
+ function readJsonBody(req) {
65
+ return new Promise((resolve, reject) => {
66
+ let raw = '';
67
+ req.on('data', (chunk) => {
68
+ raw += chunk;
69
+ });
70
+ req.on('end', () => {
71
+ try {
72
+ resolve(JSON.parse(raw));
73
+ } catch {
74
+ resolve(undefined);
75
+ }
76
+ });
77
+ req.on('error', reject);
78
+ });
79
+ }
80
+
81
+ // sessionId → { transport: StreamableHTTPServerTransport, server: McpServer }
82
+ const sessions = new Map();
83
+
84
+ const httpServer = createHttpServer(async (req, res) => {
85
+ try {
86
+ const path = req.url?.split('?')[0];
87
+
88
+ if (path !== '/mcp') {
89
+ res.writeHead(404).end();
90
+ return;
91
+ }
92
+
93
+ const sessionId = req.headers['mcp-session-id'];
94
+
95
+ if (sessionId) {
96
+ // Existing session handles subsequent POST, GET (SSE), and DELETE requests.
97
+ const session = sessions.get(sessionId);
98
+ if (!session) {
99
+ res.writeHead(404).end('Session not found');
100
+ return;
101
+ }
102
+ await session.transport.handleRequest(req, res);
103
+ } else if (req.method === 'POST') {
104
+ // New session: the first request must be an MCP initialize.
105
+ const parsedBody = await readJsonBody(req);
106
+ const newSessionId = randomUUID();
107
+ const transport = new StreamableHTTPServerTransport({
108
+ sessionIdGenerator: () => newSessionId,
109
+ });
110
+ const server = makeMcpServer();
111
+
112
+ transport.onclose = () => sessions.delete(newSessionId);
113
+
114
+ await server.connect(transport);
115
+ sessions.set(newSessionId, { transport, server });
116
+
117
+ await transport.handleRequest(req, res, parsedBody);
118
+ } else {
119
+ res.writeHead(405).end('Method not allowed');
120
+ }
121
+ } catch (err) {
122
+ if (!res.headersSent) res.writeHead(500).end(String(err));
123
+ }
124
+ });
125
+
126
+ await new Promise((resolve, reject) => {
127
+ httpServer.listen(effectivePort, '127.0.0.1', resolve);
128
+ httpServer.on('error', reject);
129
+ });
130
+
131
+ // Wrap close() so the upstream subprocess is killed when the HTTP server shuts down.
132
+ const _close = httpServer.close.bind(httpServer);
133
+ httpServer.close = (cb) => {
134
+ _close((err) => {
135
+ upstreamClient
136
+ .close()
137
+ .catch(() => {})
138
+ .then(() => cb?.(err));
139
+ });
140
+ return httpServer;
141
+ };
142
+
143
+ return httpServer;
144
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@injectivelabs/ainj",
3
+ "version": "0.0.1",
4
+ "description": "An AI SDK for Injective",
5
+ "keywords": [
6
+ "injective",
7
+ "AI",
8
+ "agent-skills",
9
+ "skills",
10
+ "MCP",
11
+ "CLI",
12
+ "SDK"
13
+ ],
14
+ "engines": {
15
+ "node": ">=24.0.0"
16
+ },
17
+ "homepage": "https://github.com/InjectiveLabs/ainj#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/InjectiveLabs/ainj/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/InjectiveLabs/ainj.git"
24
+ },
25
+ "license": "Apache-2.0",
26
+ "author": "Injective Labs",
27
+ "type": "module",
28
+ "main": "dist/lib/ainj.js",
29
+ "bin": {
30
+ "ainj": "dist/cli/index.js"
31
+ },
32
+ "files": [
33
+ "ainj.config.json",
34
+ "dist/",
35
+ "scripts/sync-skills.js"
36
+ ],
37
+ "dependencies": {
38
+ "@clack/prompts": "1.7.0",
39
+ "@injective-agent/core": "github:InjectiveLabs/mcp-server#f5af39367975872a85b5447cefc9a197f2e635ea",
40
+ "@modelcontextprotocol/sdk": "1.29.0",
41
+ "injective-core": "1.20.1",
42
+ "smol-toml": "1.7.0"
43
+ },
44
+ "devDependencies": {
45
+ "@biomejs/biome": "2.5.3",
46
+ "c8": "11.0.0",
47
+ "typescript": "7.0.2",
48
+ "vitest": "4.1.10"
49
+ },
50
+ "overrides": {
51
+ "@cosmjs/amino": "0.39.0",
52
+ "@cosmjs/crypto": "0.39.0",
53
+ "@cosmjs/encoding": "0.39.0",
54
+ "@cosmjs/json-rpc": "0.39.0",
55
+ "@cosmjs/math": "0.39.0",
56
+ "@cosmjs/proto-signing": "0.39.0",
57
+ "@cosmjs/socket": "0.39.0",
58
+ "@cosmjs/stargate": "0.39.0",
59
+ "@cosmjs/stream": "0.39.0",
60
+ "@cosmjs/tendermint-rpc": "0.39.0",
61
+ "@cosmjs/utils": "0.39.0",
62
+ "cosmjs-types": "0.11.0"
63
+ },
64
+ "scripts": {
65
+ "build": "node scripts/build-package.js",
66
+ "test": "node --test --experimental-test-module-mocks",
67
+ "lint": "biome check .",
68
+ "sync:skills": "node scripts/sync-skills.js",
69
+ "list-files": "npm pack && tar -xvzf *.tgz && rm -rf package *.tgz"
70
+ }
71
+ }
@@ -0,0 +1,66 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { cp, mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const thisDir = import.meta.dirname;
9
+
10
+ export async function syncSkills({
11
+ _readFile = readFile,
12
+ _spawn = spawnSync,
13
+ _cp = (src, dest, opts) => cp(src, dest, opts),
14
+ _rm = rm,
15
+ _mkdir = mkdir,
16
+ _mkdtemp = mkdtemp,
17
+ _env = process.env,
18
+ _cwd = () => process.cwd(),
19
+ } = {}) {
20
+ console.log('Syncing skills...');
21
+ const configText = await _readFile(path.join(thisDir, '..', 'ainj.config.json'), 'utf8');
22
+ const config = JSON.parse(configText);
23
+
24
+ if (!config.skillsRef) {
25
+ throw new Error('ainj.config.json is missing required field: skillsRef');
26
+ }
27
+
28
+ const { skillsRef } = config;
29
+ console.log('Skills git ref:', skillsRef);
30
+ const token = _env.GITHUB_TOKEN;
31
+ const repoUrl = token
32
+ ? `https://${token}@github.com/InjectiveLabs/agent-skills.git`
33
+ : 'https://github.com/InjectiveLabs/agent-skills.git';
34
+
35
+ const tmpDir = await _mkdtemp(path.join(os.tmpdir(), 'ainj-skills-'));
36
+
37
+ try {
38
+ runGit(['init', tmpDir], 'init');
39
+ runGit(['-C', tmpDir, 'remote', 'add', 'origin', repoUrl], 'remote add');
40
+ runGit(['-C', tmpDir, 'fetch', '--depth=1', 'origin', skillsRef], 'fetch');
41
+ runGit(['-C', tmpDir, 'checkout', '--detach', 'FETCH_HEAD'], 'checkout');
42
+
43
+ const agentsSkillsDir = path.join(thisDir, '../.agents/skills');
44
+ await _rm(agentsSkillsDir, { recursive: true, force: true });
45
+ await _mkdir(agentsSkillsDir, { recursive: true, force: true });
46
+
47
+ const srcSkillsDir = path.join(tmpDir, 'skills');
48
+ await _cp(srcSkillsDir, agentsSkillsDir, { recursive: true, force: true });
49
+ console.log('skills copied to:', agentsSkillsDir);
50
+ } finally {
51
+ await _rm(tmpDir, { recursive: true, force: true });
52
+ }
53
+
54
+ function runGit(args, action) {
55
+ const result = _spawn('git', args, { stdio: 'pipe' });
56
+ if (result.status !== 0) {
57
+ const stderr = result.stderr?.toString?.() ?? '';
58
+ throw new Error(`git ${action} failed (exit ${result.status}): ${stderr}`);
59
+ }
60
+ }
61
+ }
62
+
63
+ const currentFilePath = fileURLToPath(import.meta.url);
64
+ if (currentFilePath === process.argv[1]) {
65
+ await syncSkills();
66
+ }