invokora 0.1.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.
Files changed (41) hide show
  1. package/dist/cli/app.d.ts +55 -0
  2. package/dist/cli/app.js +1087 -0
  3. package/dist/cli/config.d.ts +12 -0
  4. package/dist/cli/config.js +73 -0
  5. package/dist/cli/constants.d.ts +24 -0
  6. package/dist/cli/constants.js +52 -0
  7. package/dist/cli/http.d.ts +2 -0
  8. package/dist/cli/http.js +23 -0
  9. package/dist/cli/index.d.ts +6 -0
  10. package/dist/cli/index.js +11 -0
  11. package/dist/cli/mcp/app.d.ts +12 -0
  12. package/dist/cli/mcp/app.js +85 -0
  13. package/dist/cli/mcp/backend_client.d.ts +10 -0
  14. package/dist/cli/mcp/backend_client.js +91 -0
  15. package/dist/cli/mcp/errors.d.ts +28 -0
  16. package/dist/cli/mcp/errors.js +139 -0
  17. package/dist/cli/mcp/progress.d.ts +12 -0
  18. package/dist/cli/mcp/progress.js +49 -0
  19. package/dist/cli/mcp/responses_session.d.ts +21 -0
  20. package/dist/cli/mcp/responses_session.js +233 -0
  21. package/dist/cli/mcp/schemas.d.ts +99 -0
  22. package/dist/cli/mcp/schemas.js +66 -0
  23. package/dist/cli/mcp/server.d.ts +4 -0
  24. package/dist/cli/mcp/server.js +3 -0
  25. package/dist/cli/mcp/session_store.d.ts +32 -0
  26. package/dist/cli/mcp/session_store.js +58 -0
  27. package/dist/cli/mcp/tool_handlers.d.ts +3 -0
  28. package/dist/cli/mcp/tool_handlers.js +26 -0
  29. package/dist/cli/mcp_setup.d.ts +33 -0
  30. package/dist/cli/mcp_setup.js +225 -0
  31. package/dist/cli/oauth.d.ts +45 -0
  32. package/dist/cli/oauth.js +594 -0
  33. package/dist/cli/prompts.d.ts +23 -0
  34. package/dist/cli/prompts.js +175 -0
  35. package/dist/cli/release.d.ts +3 -0
  36. package/dist/cli/release.js +3 -0
  37. package/dist/cli/skills.d.ts +43 -0
  38. package/dist/cli/skills.js +443 -0
  39. package/dist/cli/types.d.ts +183 -0
  40. package/dist/cli/types.js +1 -0
  41. package/package.json +29 -0
@@ -0,0 +1,225 @@
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { DEFAULT_MCP_CONFIG_PATHS } from './constants.js';
4
+ const MCP_SERVER_NAME = 'invokora';
5
+ const CLI_NPM_PACKAGE = 'invokora';
6
+ export class McpConfigService {
7
+ prompts;
8
+ constructor(prompts) {
9
+ this.prompts = prompts;
10
+ }
11
+ buildServerConfig(_config) {
12
+ return {
13
+ command: 'npx',
14
+ args: ['-y', CLI_NPM_PACKAGE, 'mcp', 'serve'],
15
+ };
16
+ }
17
+ parseSetupArgs(args) {
18
+ const targets = [];
19
+ const paths = [];
20
+ let yes = false;
21
+ for (let i = 0; i < args.length; i++) {
22
+ const arg = args[i];
23
+ if (arg === '--target') {
24
+ const value = args[i + 1];
25
+ if (!value)
26
+ throw new Error('Missing value for --target');
27
+ if (!this.isMcpTarget(value))
28
+ throw new Error(`Unsupported target: ${value}`);
29
+ targets.push(value);
30
+ i++;
31
+ continue;
32
+ }
33
+ if (arg === '--yes') {
34
+ yes = true;
35
+ continue;
36
+ }
37
+ if (arg === '--path') {
38
+ const value = args[i + 1];
39
+ if (!value)
40
+ throw new Error('Missing value for --path');
41
+ paths.push(value);
42
+ i++;
43
+ continue;
44
+ }
45
+ throw new Error(`Unknown mcp setup option: ${arg}`);
46
+ }
47
+ return {
48
+ targets: this.uniqueTargets(targets),
49
+ yes,
50
+ paths,
51
+ };
52
+ }
53
+ async resolveTargets(parsed) {
54
+ if (parsed.targets.length > 0) {
55
+ return parsed.targets;
56
+ }
57
+ if (parsed.yes) {
58
+ throw new Error('--yes requires at least one --target');
59
+ }
60
+ return this.prompts.pickMcpTargets();
61
+ }
62
+ mapPathsToTargets(targets, paths) {
63
+ const mapping = new Map();
64
+ if (paths.length === 0)
65
+ return mapping;
66
+ if (targets.length === 0) {
67
+ throw new Error('--path requires explicit --target in non-interactive mode');
68
+ }
69
+ if (paths.length === 1 && targets.length === 1) {
70
+ mapping.set(targets[0], paths[0]);
71
+ return mapping;
72
+ }
73
+ if (paths.length === targets.length) {
74
+ targets.forEach((target, index) => mapping.set(target, paths[index]));
75
+ return mapping;
76
+ }
77
+ throw new Error('--path count must be 1 (for single target) or match --target count');
78
+ }
79
+ async setupTargetConfig(params) {
80
+ const pathToWrite = params.explicitPath ?? this.resolveClientConfigPath(params.target);
81
+ if (params.target === 'codex') {
82
+ const exists = existsSync(pathToWrite);
83
+ if (!exists && !params.explicitPath && params.yes) {
84
+ throw new Error('Codex config not found. Use --path to create in non-interactive mode.');
85
+ }
86
+ if (!exists && !params.yes) {
87
+ const confirmed = await this.prompts.yesNo(`Codex config not found at ${pathToWrite}. Create it?`, false);
88
+ if (!confirmed)
89
+ return { status: 'skipped', path: pathToWrite };
90
+ }
91
+ this.writeCodexMcpConfig(pathToWrite, params.serverConfig);
92
+ return { status: 'updated', path: pathToWrite };
93
+ }
94
+ this.writeJsonMcpConfig(pathToWrite, params.serverConfig);
95
+ return { status: 'updated', path: pathToWrite };
96
+ }
97
+ resolveClientConfigPath(target) {
98
+ return DEFAULT_MCP_CONFIG_PATHS[target]();
99
+ }
100
+ writeJsonMcpConfig(filePath, serverConfig) {
101
+ const current = this.loadOrInitMcpConfig(filePath);
102
+ const next = this.upsertInvokoraServer(current, serverConfig);
103
+ this.writeSecureFile(filePath, JSON.stringify(next, null, 2) + '\n');
104
+ }
105
+ loadOrInitMcpConfig(filePath) {
106
+ if (!existsSync(filePath))
107
+ return {};
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(readFileSync(filePath, 'utf-8'));
111
+ }
112
+ catch {
113
+ throw new Error(`Invalid JSON in ${filePath}`);
114
+ }
115
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
116
+ throw new Error(`Invalid MCP JSON root in ${filePath}`);
117
+ }
118
+ return parsed;
119
+ }
120
+ upsertInvokoraServer(configJson, serverConfig) {
121
+ const next = { ...configJson };
122
+ const rawServers = next.mcpServers;
123
+ const servers = rawServers && typeof rawServers === 'object' && !Array.isArray(rawServers) ? { ...rawServers } : {};
124
+ // setup 只接管当前 Invokora 入口,不能顺手覆盖用户别的 MCP 服务配置。
125
+ servers[MCP_SERVER_NAME] = serverConfig;
126
+ next.mcpServers = servers;
127
+ return next;
128
+ }
129
+ writeCodexMcpConfig(filePath, serverConfig) {
130
+ const existing = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
131
+ if (existing && this.isLikelyJson(existing)) {
132
+ throw new Error(`Codex config expects TOML, but ${filePath} looks like JSON`);
133
+ }
134
+ const section = this.renderCodexInvokoraSection(serverConfig);
135
+ const updated = this.upsertTomlSection(existing, `mcp_servers.${MCP_SERVER_NAME}`, section);
136
+ this.writeSecureFile(filePath, updated);
137
+ }
138
+ writeSecureFile(filePath, content) {
139
+ mkdirSync(dirname(filePath), { recursive: true });
140
+ const mode = this.resolveSecureFileMode(filePath);
141
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
142
+ try {
143
+ writeFileSync(tmpPath, content, { encoding: 'utf-8', mode });
144
+ renameSync(tmpPath, filePath);
145
+ }
146
+ catch (error) {
147
+ rmSync(tmpPath, { force: true });
148
+ throw error;
149
+ }
150
+ }
151
+ resolveSecureFileMode(filePath) {
152
+ if (!existsSync(filePath)) {
153
+ return 0o600;
154
+ }
155
+ const currentMode = lstatSync(filePath).mode & 0o777;
156
+ const tightenedMode = currentMode & 0o600;
157
+ return tightenedMode === 0 ? 0o600 : tightenedMode;
158
+ }
159
+ renderCodexInvokoraSection(serverConfig) {
160
+ const envEntries = Object.entries(serverConfig.env ?? {})
161
+ .map(([key, value]) => `${key} = "${this.escapeTomlString(value)}"`)
162
+ .join(', ');
163
+ const lines = [
164
+ `[mcp_servers.${MCP_SERVER_NAME}]`,
165
+ `command = "${this.escapeTomlString(serverConfig.command)}"`,
166
+ `args = [${serverConfig.args.map((arg) => `"${this.escapeTomlString(arg)}"`).join(', ')}]`,
167
+ ];
168
+ if (envEntries !== '') {
169
+ lines.push(`env = { ${envEntries} }`);
170
+ }
171
+ return lines.join('\n');
172
+ }
173
+ escapeTomlString(value) {
174
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
175
+ }
176
+ isLikelyJson(content) {
177
+ const trimmed = content.trimStart();
178
+ if (trimmed.startsWith('{'))
179
+ return true;
180
+ if (!trimmed.startsWith('['))
181
+ return false;
182
+ try {
183
+ JSON.parse(trimmed);
184
+ return true;
185
+ }
186
+ catch {
187
+ return false;
188
+ }
189
+ }
190
+ upsertTomlSection(content, sectionHeader, sectionContent) {
191
+ const normalized = content.replace(/\r\n/g, '\n');
192
+ const lines = normalized.split('\n');
193
+ const header = `[${sectionHeader}]`;
194
+ let start = -1;
195
+ for (let i = 0; i < lines.length; i++) {
196
+ if (lines[i].trim() === header) {
197
+ start = i;
198
+ break;
199
+ }
200
+ }
201
+ const replacementLines = sectionContent.trimEnd().split('\n');
202
+ if (start >= 0) {
203
+ let end = lines.length;
204
+ for (let i = start + 1; i < lines.length; i++) {
205
+ const trimmed = lines[i].trim();
206
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
207
+ end = i;
208
+ break;
209
+ }
210
+ }
211
+ const merged = [...lines.slice(0, start), ...replacementLines, ...lines.slice(end)].join('\n').trimEnd();
212
+ return `${merged}\n`;
213
+ }
214
+ const base = normalized.trimEnd();
215
+ if (base.length === 0)
216
+ return `${sectionContent.trimEnd()}\n`;
217
+ return `${base}\n\n${sectionContent.trimEnd()}\n`;
218
+ }
219
+ isMcpTarget(value) {
220
+ return value === 'claude' || value === 'cursor' || value === 'codex';
221
+ }
222
+ uniqueTargets(targets) {
223
+ return [...new Set(targets)];
224
+ }
225
+ }
@@ -0,0 +1,45 @@
1
+ import type { LoginCommandOptions, OAuthCallbackPayload, OAuthExchangeResponse, OAuthLoopbackListener, OAuthStartResponse, RunOAuthBrowserLoginParams } from './types.js';
2
+ export declare function base64UrlEncode(input: Buffer): string;
3
+ export declare function generateOAuthState(): string;
4
+ export declare function generateCodeVerifier(): string;
5
+ export declare function buildS256CodeChallenge(codeVerifier: string): string;
6
+ export declare function parseLoginCommandOptions(args: string[]): LoginCommandOptions;
7
+ export declare function chooseLoopbackHost(address: string): string;
8
+ export declare function openBrowserURL(url: string): Promise<boolean>;
9
+ export declare function normalizeCallbackError(payload: OAuthCallbackPayload): string;
10
+ export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutMessage: string): Promise<T>;
11
+ export declare class OAuthLoopbackServer implements OAuthLoopbackListener {
12
+ private readonly expectedState;
13
+ private readonly timeoutMs;
14
+ private readonly server;
15
+ private closed;
16
+ private settled;
17
+ private redirectUrl;
18
+ private resolveCallback;
19
+ private readonly callbackPromise;
20
+ private constructor();
21
+ static create(expectedState: string, timeoutMs: number): Promise<OAuthLoopbackServer>;
22
+ get redirectURI(): string;
23
+ waitForCallback(): Promise<OAuthCallbackPayload>;
24
+ close(): Promise<void>;
25
+ private listen;
26
+ private buildRedirectURI;
27
+ private complete;
28
+ private handleRequest;
29
+ }
30
+ export declare function createOAuthLoopbackListener(expectedState: string, timeoutMs: number): Promise<OAuthLoopbackListener>;
31
+ export declare class OAuthCLIClient {
32
+ private readonly backendUrl;
33
+ private readonly fetchImpl;
34
+ constructor(backendUrl: string, fetchImpl?: typeof fetch);
35
+ start(redirectURI: string, state: string, codeChallenge: string): Promise<OAuthStartResponse>;
36
+ exchange(state: string, codeVerifier: string, code: string): Promise<OAuthExchangeResponse>;
37
+ }
38
+ export declare function startOAuthCLI(backendUrl: string, redirectURI: string, state: string, codeChallenge: string, fetchImpl: typeof fetch): Promise<OAuthStartResponse>;
39
+ export declare function exchangeOAuthCode(backendUrl: string, state: string, codeVerifier: string, code: string, fetchImpl: typeof fetch): Promise<OAuthExchangeResponse>;
40
+ export declare class OAuthBrowserLoginFlow {
41
+ private readonly params;
42
+ constructor(params: RunOAuthBrowserLoginParams);
43
+ run(): Promise<OAuthExchangeResponse>;
44
+ }
45
+ export declare function runOAuthBrowserLogin(params: RunOAuthBrowserLoginParams): Promise<OAuthExchangeResponse>;