insta 0.0.20 → 0.0.21
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/dist/commands/mcp.js +136 -0
- package/dist/commands/setup.js +57 -1
- package/dist/index.js +8 -0
- package/package.json +2 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// `insta mcp install` — write the insta-cloud remote MCP server into each coding agent's own
|
|
2
|
+
// config format. Claude Code is NOT handled here — it has a real registry CLI (`claude mcp add`,
|
|
3
|
+
// see setup.ts registerMcp); these are the config-file agents. All entries are OAuth (no
|
|
4
|
+
// credential written): each client discovers the platform AS via RFC 9728 and runs the browser
|
|
5
|
+
// flow on first use.
|
|
6
|
+
import { promises as fs } from 'node:fs';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { info } from '../util.js';
|
|
11
|
+
import { DEFAULT_MCP_URL, MCP_SERVER_NAME, registerMcp } from './setup.js';
|
|
12
|
+
export const MCP_AGENT_TARGETS = ['cursor', 'codex', 'opencode', 'copilot', 'factory-droid'];
|
|
13
|
+
export function configPath(slug, home) {
|
|
14
|
+
switch (slug) {
|
|
15
|
+
case 'cursor': return path.join(home, '.cursor', 'mcp.json');
|
|
16
|
+
case 'codex': return path.join(home, '.codex', 'config.toml');
|
|
17
|
+
case 'opencode': return path.join(home, '.config', 'opencode', 'opencode.json');
|
|
18
|
+
case 'copilot': return path.join(home, '.copilot', 'mcp-config.json');
|
|
19
|
+
case 'factory-droid': return path.join(home, '.factory', 'mcp.json');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// An agent counts as "on this machine" when its config dir already exists — we configure what's
|
|
23
|
+
// installed, never scaffold a tool the user doesn't have.
|
|
24
|
+
export function detectAgents(home) {
|
|
25
|
+
return MCP_AGENT_TARGETS.filter((slug) => existsSync(path.dirname(configPath(slug, home))));
|
|
26
|
+
}
|
|
27
|
+
// Merge our entry into existing JSON config. Returns null (skip, leave file alone) when the
|
|
28
|
+
// existing content isn't valid JSON — never clobber a config we can't parse.
|
|
29
|
+
export function renderJsonConfig(slug, existing, url) {
|
|
30
|
+
let root = {};
|
|
31
|
+
if (existing && existing.trim()) {
|
|
32
|
+
try {
|
|
33
|
+
root = JSON.parse(existing);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (typeof root !== 'object' || root === null || Array.isArray(root))
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (slug === 'opencode') {
|
|
42
|
+
// OpenCode: `mcp` key, `type: "remote"` schema (docs.opencode.ai).
|
|
43
|
+
root.mcp = { ...(root.mcp ?? {}), [MCP_SERVER_NAME]: { type: 'remote', url, enabled: true } };
|
|
44
|
+
root.$schema ??= 'https://opencode.ai/config.json';
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const entry = slug === 'cursor' ? { url } // Cursor auto-detects HTTP from `url`
|
|
48
|
+
: slug === 'copilot' ? { type: 'http', url, tools: ['*'] }
|
|
49
|
+
: { type: 'http', url, disabled: false }; // factory-droid
|
|
50
|
+
root.mcpServers = { ...(root.mcpServers ?? {}), [MCP_SERVER_NAME]: entry };
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(root, null, 2) + '\n';
|
|
53
|
+
}
|
|
54
|
+
// Codex config is TOML. Appending a complete `[mcp_servers.<name>]` table is always valid at
|
|
55
|
+
// EOF, so we avoid a TOML parser: string-detect for idempotency, append for install.
|
|
56
|
+
export function renderCodexConfig(existing, url) {
|
|
57
|
+
const base = existing ?? '';
|
|
58
|
+
if (base.includes(`[mcp_servers.${MCP_SERVER_NAME}]`))
|
|
59
|
+
return null; // already configured
|
|
60
|
+
const sep = base.length && !base.endsWith('\n') ? '\n' : '';
|
|
61
|
+
return `${base}${sep}\n[mcp_servers.${MCP_SERVER_NAME}]\nurl = "${url}"\n`;
|
|
62
|
+
}
|
|
63
|
+
// Install for one agent. Returns 'installed' | 'already' | 'skipped' (unparseable config).
|
|
64
|
+
export async function installFor(slug, home, url) {
|
|
65
|
+
const file = configPath(slug, home);
|
|
66
|
+
let existing = null;
|
|
67
|
+
try {
|
|
68
|
+
existing = await fs.readFile(file, 'utf8');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
existing = null;
|
|
72
|
+
}
|
|
73
|
+
if (slug === 'codex') {
|
|
74
|
+
const next = renderCodexConfig(existing, url);
|
|
75
|
+
if (next === null)
|
|
76
|
+
return 'already';
|
|
77
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
78
|
+
await fs.writeFile(file, next);
|
|
79
|
+
return 'installed';
|
|
80
|
+
}
|
|
81
|
+
if (existing) {
|
|
82
|
+
try {
|
|
83
|
+
const root = JSON.parse(existing);
|
|
84
|
+
const entry = slug === 'opencode' ? root?.mcp?.[MCP_SERVER_NAME] : root?.mcpServers?.[MCP_SERVER_NAME];
|
|
85
|
+
if (entry)
|
|
86
|
+
return 'already';
|
|
87
|
+
}
|
|
88
|
+
catch { /* fall through to renderJsonConfig, which refuses to clobber */ }
|
|
89
|
+
}
|
|
90
|
+
const next = renderJsonConfig(slug, existing, url);
|
|
91
|
+
if (next === null)
|
|
92
|
+
return 'skipped';
|
|
93
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
94
|
+
await fs.writeFile(file, next);
|
|
95
|
+
return 'installed';
|
|
96
|
+
}
|
|
97
|
+
const AGENT_LABELS = {
|
|
98
|
+
cursor: 'Cursor', codex: 'OpenAI Codex', opencode: 'OpenCode', copilot: 'GitHub Copilot', 'factory-droid': 'Factory Droid',
|
|
99
|
+
};
|
|
100
|
+
// Configure every detected config-file agent (or one forced via `agent`). Returns the labels of
|
|
101
|
+
// agents now configured (installed or already present) for the caller's summary line.
|
|
102
|
+
export async function installAgentConfigs(agent, home = os.homedir()) {
|
|
103
|
+
const url = process.env.INSTA_MCP_URL || DEFAULT_MCP_URL;
|
|
104
|
+
const targets = agent
|
|
105
|
+
? MCP_AGENT_TARGETS.includes(agent) ? [agent] : []
|
|
106
|
+
: detectAgents(home);
|
|
107
|
+
if (agent && targets.length === 0) {
|
|
108
|
+
info(`unknown --agent "${agent}" — supported: claude-code, ${MCP_AGENT_TARGETS.join(', ')}`);
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
const done = [];
|
|
112
|
+
for (const slug of targets) {
|
|
113
|
+
const result = await installFor(slug, home, url);
|
|
114
|
+
if (result === 'skipped')
|
|
115
|
+
info(` ${AGENT_LABELS[slug]}: existing config at ${configPath(slug, home)} isn't valid JSON — add ${MCP_SERVER_NAME} manually`);
|
|
116
|
+
else
|
|
117
|
+
done.push(AGENT_LABELS[slug]);
|
|
118
|
+
}
|
|
119
|
+
return done;
|
|
120
|
+
}
|
|
121
|
+
// `insta mcp install [--agent <slug>] [--mcp-token]` — claude-code goes through its registry CLI
|
|
122
|
+
// (registerMcp); everything else is a config-file write. No --agent = claude-code + all detected.
|
|
123
|
+
export async function mcpInstall(opts) {
|
|
124
|
+
if (!opts.agent || opts.agent === 'claude-code') {
|
|
125
|
+
await registerMcp(undefined, undefined, !!opts.mcpToken);
|
|
126
|
+
if (opts.agent)
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const done = await installAgentConfigs(opts.agent);
|
|
130
|
+
if (done.length)
|
|
131
|
+
info(`✓ MCP — configured for ${done.join(', ')} (restart those tools to pick it up)`);
|
|
132
|
+
else if (opts.agent) { /* messages already printed */ }
|
|
133
|
+
else
|
|
134
|
+
info(' no other MCP-capable agents detected (supported: cursor, codex, opencode, copilot, factory-droid)');
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=mcp.js.map
|
package/dist/commands/setup.js
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
// Stack skills (neon/tigris/better-auth) intentionally stay per-project: their presence in a
|
|
7
7
|
// project doubles as its stack manifest — that install happens on `project create|link`.
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import { ApiClient } from '../api.js';
|
|
9
11
|
import { info } from '../util.js';
|
|
12
|
+
import { installAgentConfigs } from './mcp.js';
|
|
10
13
|
// The `skills` tool we shell out to prints a clack UI: a frame-by-frame clone spinner, an
|
|
11
14
|
// "Installing to all N agents" banner, a full N-line install-path box, and a third-party
|
|
12
15
|
// "Security Risk Assessment" that flags our OWN first-party skill as "Critical Risk". Streamed
|
|
@@ -89,7 +92,56 @@ const defaultRunner = (cmd, args) => new Promise((resolve) => {
|
|
|
89
92
|
// -g = user-level (machine-global); -a '*' = every agent dir the skills tool supports
|
|
90
93
|
// (Claude Code, Codex, Cursor, OpenCode, Copilot, …); --copy = real files, not cache symlinks.
|
|
91
94
|
export const SETUP_ARGS = ['skills', 'add', 'InsForge/insta-skills', '-s', 'insta', '-a', '*', '-g', '-y', '--copy'];
|
|
92
|
-
|
|
95
|
+
// ---- remote MCP registration ----
|
|
96
|
+
export const MCP_SERVER_NAME = 'insta-cloud';
|
|
97
|
+
export const DEFAULT_MCP_URL = 'https://mcp.instacloud.com/mcp';
|
|
98
|
+
const defaultMinter = async () => {
|
|
99
|
+
try {
|
|
100
|
+
const api = await ApiClient.load();
|
|
101
|
+
if (!api.config.accessToken)
|
|
102
|
+
return null;
|
|
103
|
+
const { token } = await api.request('POST', '/tokens', { name: `mcp-${os.hostname()}` });
|
|
104
|
+
return token ?? null;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
// Register the insta-cloud remote MCP server with Claude Code (user scope, so it follows the
|
|
111
|
+
// machine like the skill install above). Default is OAuth: register with NO credential — the
|
|
112
|
+
// platform's Better Auth MCP authorization server is discovered via RFC 9728 and Claude runs
|
|
113
|
+
// the browser flow on first `/mcp` use, so no static token ever lands on disk. `--mcp-token`
|
|
114
|
+
// is the headless fallback (CI, no browser): mint a durable token into the header instead.
|
|
115
|
+
// Idempotent — an existing registration is left alone. Best-effort: the skill install is the
|
|
116
|
+
// primary outcome; agents without an MCP registry are covered by the skill alone.
|
|
117
|
+
export async function registerMcp(run = defaultRunner, mint = defaultMinter, useToken = false) {
|
|
118
|
+
const url = process.env.INSTA_MCP_URL || DEFAULT_MCP_URL;
|
|
119
|
+
if (!(await run('claude', ['--version'])).ok)
|
|
120
|
+
return; // no Claude Code on this machine
|
|
121
|
+
if ((await run('claude', ['mcp', 'get', MCP_SERVER_NAME])).ok) {
|
|
122
|
+
info(`✓ MCP — ${MCP_SERVER_NAME} already registered with Claude Code`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const args = ['mcp', 'add', '--transport', 'http', '--scope', 'user', MCP_SERVER_NAME, url];
|
|
126
|
+
if (useToken) {
|
|
127
|
+
const token = await mint();
|
|
128
|
+
if (!token) {
|
|
129
|
+
info(' MCP not registered (--mcp-token needs a login) — run `insta login`, then `insta setup agent --mcp-token` again');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
args.push('--header', `Authorization: Bearer ${token}`);
|
|
133
|
+
}
|
|
134
|
+
const res = await run('claude', args);
|
|
135
|
+
if (res.ok) {
|
|
136
|
+
info(`✓ MCP — ${MCP_SERVER_NAME} registered with Claude Code (\`claude mcp list\` to verify)`);
|
|
137
|
+
if (!useToken)
|
|
138
|
+
info(' first use: run `/mcp` in Claude Code and authorize in the browser (headless machines: `insta setup agent --mcp-token`)');
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
info(` MCP registration failed — add manually:\n claude mcp add --transport http ${MCP_SERVER_NAME} ${url}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export async function setupAgent(opts, run = defaultRunner, mint, installConfigs = installAgentConfigs) {
|
|
93
145
|
if (!opts.yes && !process.stdout.isTTY) {
|
|
94
146
|
info('non-interactive shell — assuming -y');
|
|
95
147
|
}
|
|
@@ -111,5 +163,9 @@ export async function setupAgent(opts, run = defaultRunner) {
|
|
|
111
163
|
}
|
|
112
164
|
info(summarizeInstall(res.output ?? ''));
|
|
113
165
|
info(' every coding agent on this machine now knows InstaCloud (review skills before use — they run with full permissions).');
|
|
166
|
+
await registerMcp(run, mint, !!opts.mcpToken);
|
|
167
|
+
const others = await installConfigs();
|
|
168
|
+
if (others.length)
|
|
169
|
+
info(`✓ MCP — also configured for ${others.join(', ')} (restart those tools to pick it up)`);
|
|
114
170
|
}
|
|
115
171
|
//# sourceMappingURL=setup.js.map
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ApiError } from './api.js';
|
|
|
5
5
|
import { die } from './util.js';
|
|
6
6
|
import * as auth from './commands/auth.js';
|
|
7
7
|
import * as setup from './commands/setup.js';
|
|
8
|
+
import * as mcp from './commands/mcp.js';
|
|
8
9
|
import * as runCmd from './commands/run.js';
|
|
9
10
|
import * as org from './commands/org.js';
|
|
10
11
|
import * as project from './commands/project.js';
|
|
@@ -66,7 +67,14 @@ program.command('run <cmd> [args...]').description('Run a command with the branc
|
|
|
66
67
|
const setupCmd = program.command('setup').description('Set up this machine for InstaCloud agent workflows');
|
|
67
68
|
setupCmd.command('agent').description('Install the insta skill user-globally for all coding agents')
|
|
68
69
|
.option('-y, --yes', 'non-interactive')
|
|
70
|
+
.option('--mcp-token', 'register the MCP server with a minted insta_ API token instead of OAuth (headless machines / CI)')
|
|
69
71
|
.action(guard((o) => setup.setupAgent(o)));
|
|
72
|
+
// ---- MCP server integration ----
|
|
73
|
+
const mcpCmd = program.command('mcp').description('insta-cloud remote MCP server integration');
|
|
74
|
+
mcpCmd.command('install').description('Register the remote MCP server with coding agents (default: Claude Code + all detected)')
|
|
75
|
+
.option('--agent <slug>', 'one agent: claude-code, cursor, codex, opencode, copilot, factory-droid')
|
|
76
|
+
.option('--mcp-token', 'claude-code only: minted insta_ API token instead of OAuth (headless machines / CI)')
|
|
77
|
+
.action(guard((o) => mcp.mcpInstall(o)));
|
|
70
78
|
// ---- org ----
|
|
71
79
|
const orgCmd = program.command('org').description('Manage organizations');
|
|
72
80
|
orgCmd.command('list').option('--json').action(guard((o) => org.orgList(o)));
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "insta",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "InstaCloud CLI
|
|
5
|
+
"description": "InstaCloud CLI \u2014 a thin client of the platform control-plane API.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"insta",
|
|
8
8
|
"insforge",
|