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.
- package/dist/cli/app.d.ts +55 -0
- package/dist/cli/app.js +1087 -0
- package/dist/cli/config.d.ts +12 -0
- package/dist/cli/config.js +73 -0
- package/dist/cli/constants.d.ts +24 -0
- package/dist/cli/constants.js +52 -0
- package/dist/cli/http.d.ts +2 -0
- package/dist/cli/http.js +23 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +11 -0
- package/dist/cli/mcp/app.d.ts +12 -0
- package/dist/cli/mcp/app.js +85 -0
- package/dist/cli/mcp/backend_client.d.ts +10 -0
- package/dist/cli/mcp/backend_client.js +91 -0
- package/dist/cli/mcp/errors.d.ts +28 -0
- package/dist/cli/mcp/errors.js +139 -0
- package/dist/cli/mcp/progress.d.ts +12 -0
- package/dist/cli/mcp/progress.js +49 -0
- package/dist/cli/mcp/responses_session.d.ts +21 -0
- package/dist/cli/mcp/responses_session.js +233 -0
- package/dist/cli/mcp/schemas.d.ts +99 -0
- package/dist/cli/mcp/schemas.js +66 -0
- package/dist/cli/mcp/server.d.ts +4 -0
- package/dist/cli/mcp/server.js +3 -0
- package/dist/cli/mcp/session_store.d.ts +32 -0
- package/dist/cli/mcp/session_store.js +58 -0
- package/dist/cli/mcp/tool_handlers.d.ts +3 -0
- package/dist/cli/mcp/tool_handlers.js +26 -0
- package/dist/cli/mcp_setup.d.ts +33 -0
- package/dist/cli/mcp_setup.js +225 -0
- package/dist/cli/oauth.d.ts +45 -0
- package/dist/cli/oauth.js +594 -0
- package/dist/cli/prompts.d.ts +23 -0
- package/dist/cli/prompts.js +175 -0
- package/dist/cli/release.d.ts +3 -0
- package/dist/cli/release.js +3 -0
- package/dist/cli/skills.d.ts +43 -0
- package/dist/cli/skills.js +443 -0
- package/dist/cli/types.d.ts +183 -0
- package/dist/cli/types.js +1 -0
- package/package.json +29 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Config } from './types.js';
|
|
2
|
+
export declare class ConfigStore {
|
|
3
|
+
private readonly configPath;
|
|
4
|
+
constructor(configPath?: string);
|
|
5
|
+
get path(): string;
|
|
6
|
+
load(): Config | null;
|
|
7
|
+
save(config: Config): void;
|
|
8
|
+
require(): Config;
|
|
9
|
+
private ensurePathSafe;
|
|
10
|
+
}
|
|
11
|
+
export declare function loadConfig(configPath?: string): Config | null;
|
|
12
|
+
export declare function saveConfig(config: Config, configPath?: string): void;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { CONFIG_PATH } from './constants.js';
|
|
4
|
+
export class ConfigStore {
|
|
5
|
+
configPath;
|
|
6
|
+
constructor(configPath = CONFIG_PATH) {
|
|
7
|
+
this.configPath = configPath;
|
|
8
|
+
}
|
|
9
|
+
get path() {
|
|
10
|
+
return this.configPath;
|
|
11
|
+
}
|
|
12
|
+
load() {
|
|
13
|
+
if (!isReadableConfigFile(this.configPath)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(this.configPath, 'utf-8'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
save(config) {
|
|
24
|
+
this.ensurePathSafe();
|
|
25
|
+
mkdirSync(dirname(this.configPath), { recursive: true });
|
|
26
|
+
// 凭证文件先落临时文件再替换,避免中断时把登录态写成半截 JSON。
|
|
27
|
+
const tmpPath = `${this.configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
28
|
+
try {
|
|
29
|
+
writeFileSync(tmpPath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
30
|
+
renameSync(tmpPath, this.configPath);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
rmSync(tmpPath, { force: true });
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
require() {
|
|
38
|
+
const config = this.load();
|
|
39
|
+
if (!config) {
|
|
40
|
+
throw new Error('Not logged in. Run: invokora login');
|
|
41
|
+
}
|
|
42
|
+
return config;
|
|
43
|
+
}
|
|
44
|
+
ensurePathSafe() {
|
|
45
|
+
try {
|
|
46
|
+
const stat = lstatSync(this.configPath);
|
|
47
|
+
// 拒绝跟随符号链接,避免凭证被写进用户未预期的位置。
|
|
48
|
+
if (stat.isSymbolicLink()) {
|
|
49
|
+
throw new Error(`Refusing to write symlink config path: ${this.configPath}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const code = error?.code;
|
|
54
|
+
if (code === 'ENOENT')
|
|
55
|
+
return;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function isReadableConfigFile(path) {
|
|
61
|
+
try {
|
|
62
|
+
return lstatSync(path).isFile();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function loadConfig(configPath = CONFIG_PATH) {
|
|
69
|
+
return new ConfigStore(configPath).load();
|
|
70
|
+
}
|
|
71
|
+
export function saveConfig(config, configPath = CONFIG_PATH) {
|
|
72
|
+
new ConfigStore(configPath).save(config);
|
|
73
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { McpTarget, McpTargetPickerOption, SyncScope, SyncTarget } from './types.js';
|
|
2
|
+
export declare const INVOKORA_HOME_DIR: string;
|
|
3
|
+
export declare const CONFIG_PATH: string;
|
|
4
|
+
export declare const MCP_SESSION_DIR: string;
|
|
5
|
+
export declare const LANGUAGE_PATH: string;
|
|
6
|
+
export declare const DEFAULT_BACKEND_URL = "https://useskilz.com";
|
|
7
|
+
export declare const API_V1 = "/v1";
|
|
8
|
+
export declare const SKILL_SHELL_MARKER = "The actual skill content is securely managed by Invokora and is never stored locally.";
|
|
9
|
+
export declare const OAUTH_CALLBACK_PATH = "/oauth/callback";
|
|
10
|
+
export declare const OAUTH_DEFAULT_TIMEOUT_MS = 120000;
|
|
11
|
+
export declare const MCP_TARGET_ALIASES: {
|
|
12
|
+
readonly '1': "claude";
|
|
13
|
+
readonly claude: "claude";
|
|
14
|
+
readonly '2': "cursor";
|
|
15
|
+
readonly cursor: "cursor";
|
|
16
|
+
readonly '3': "codex";
|
|
17
|
+
readonly codex: "codex";
|
|
18
|
+
};
|
|
19
|
+
export declare const SYNC_SCOPE_ALIASES: Record<string, SyncScope>;
|
|
20
|
+
export declare const SYNC_TARGET_ALIASES: Record<string, SyncTarget>;
|
|
21
|
+
export declare const DEFAULT_MCP_TARGET_INPUT = "1,2,3";
|
|
22
|
+
export declare const DEFAULT_SYNC_TARGET_INPUT = "1";
|
|
23
|
+
export declare const DEFAULT_MCP_CONFIG_PATHS: Record<McpTarget, () => string>;
|
|
24
|
+
export declare const MCP_TARGET_PICKER_OPTIONS: McpTargetPickerOption[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { DEFAULT_BACKEND_URL as RELEASE_DEFAULT_BACKEND_URL } from './release.js';
|
|
4
|
+
export const INVOKORA_HOME_DIR = join(homedir(), '.invokora');
|
|
5
|
+
export const CONFIG_PATH = join(INVOKORA_HOME_DIR, 'config.json');
|
|
6
|
+
export const MCP_SESSION_DIR = join(INVOKORA_HOME_DIR, 'sessions');
|
|
7
|
+
export const LANGUAGE_PATH = join(INVOKORA_HOME_DIR, 'language');
|
|
8
|
+
export const DEFAULT_BACKEND_URL = RELEASE_DEFAULT_BACKEND_URL;
|
|
9
|
+
export const API_V1 = '/v1';
|
|
10
|
+
export const SKILL_SHELL_MARKER = 'The actual skill content is securely managed by Invokora and is never stored locally.';
|
|
11
|
+
export const OAUTH_CALLBACK_PATH = '/oauth/callback';
|
|
12
|
+
export const OAUTH_DEFAULT_TIMEOUT_MS = 120_000;
|
|
13
|
+
export const MCP_TARGET_ALIASES = {
|
|
14
|
+
'1': 'claude',
|
|
15
|
+
claude: 'claude',
|
|
16
|
+
'2': 'cursor',
|
|
17
|
+
cursor: 'cursor',
|
|
18
|
+
'3': 'codex',
|
|
19
|
+
codex: 'codex',
|
|
20
|
+
};
|
|
21
|
+
export const SYNC_SCOPE_ALIASES = {
|
|
22
|
+
project: 'project',
|
|
23
|
+
p: 'project',
|
|
24
|
+
repo: 'project',
|
|
25
|
+
workspace: 'project',
|
|
26
|
+
user: 'user',
|
|
27
|
+
u: 'user',
|
|
28
|
+
global: 'user',
|
|
29
|
+
personal: 'user',
|
|
30
|
+
};
|
|
31
|
+
export const SYNC_TARGET_ALIASES = {
|
|
32
|
+
'1': 'codex',
|
|
33
|
+
codex: 'codex',
|
|
34
|
+
'2': 'claude',
|
|
35
|
+
claude: 'claude',
|
|
36
|
+
'claude-code': 'claude',
|
|
37
|
+
claudecode: 'claude',
|
|
38
|
+
'3': 'cursor',
|
|
39
|
+
cursor: 'cursor',
|
|
40
|
+
};
|
|
41
|
+
export const DEFAULT_MCP_TARGET_INPUT = '1,2,3';
|
|
42
|
+
export const DEFAULT_SYNC_TARGET_INPUT = '1';
|
|
43
|
+
export const DEFAULT_MCP_CONFIG_PATHS = {
|
|
44
|
+
claude: () => join(homedir(), '.claude.json'),
|
|
45
|
+
cursor: () => join(process.cwd(), '.cursor', 'mcp.json'),
|
|
46
|
+
codex: () => join(homedir(), '.codex', 'config.toml'),
|
|
47
|
+
};
|
|
48
|
+
export const MCP_TARGET_PICKER_OPTIONS = [
|
|
49
|
+
{ target: 'claude', label: 'Claude', path: '~/.claude.json', summary: 'Project and personal Claude config' },
|
|
50
|
+
{ target: 'cursor', label: 'Cursor', path: '.cursor/mcp.json', summary: 'Workspace-local Cursor MCP config' },
|
|
51
|
+
{ target: 'codex', label: 'Codex', path: '~/.codex/config.toml', summary: 'Global Codex MCP server config' },
|
|
52
|
+
];
|
package/dist/cli/http.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function errorMessageFromJson(json, status) {
|
|
2
|
+
if (json && typeof json === 'object' && 'error' in json) {
|
|
3
|
+
const err = json.error;
|
|
4
|
+
if (typeof err?.message === 'string')
|
|
5
|
+
return err.message;
|
|
6
|
+
}
|
|
7
|
+
return `HTTP ${status}`;
|
|
8
|
+
}
|
|
9
|
+
export async function readJsonResponse(response) {
|
|
10
|
+
const raw = await response.text();
|
|
11
|
+
const trimmed = raw.trim();
|
|
12
|
+
if (!trimmed) {
|
|
13
|
+
throw new Error(`Empty response body (HTTP ${response.status})`);
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(trimmed);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
const contentType = response.headers.get('content-type') || 'unknown content-type';
|
|
20
|
+
const snippet = trimmed.length > 160 ? `${trimmed.slice(0, 160)}...` : trimmed;
|
|
21
|
+
throw new Error(`Expected JSON response but received ${contentType} (HTTP ${response.status}): ${snippet}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export { ConfigStore, loadConfig, saveConfig } from './config.js';
|
|
3
|
+
export { cmdLogin, getUsageText, isMainModule, loadCliLanguage, main, parseCliLanguage, saveCliLanguage, InvokoraCliApp } from './app.js';
|
|
4
|
+
export type { CliLanguage } from './app.js';
|
|
5
|
+
export { base64UrlEncode, buildS256CodeChallenge, chooseLoopbackHost, createOAuthLoopbackListener, exchangeOAuthCode, generateCodeVerifier, generateOAuthState, normalizeCallbackError, OAuthBrowserLoginFlow, OAuthCLIClient, OAuthLoopbackServer, openBrowserURL, parseLoginCommandOptions, runOAuthBrowserLogin, startOAuthCLI, withTimeout, } from './oauth.js';
|
|
6
|
+
export type { AccessibleSkill, Config, LoginCommandOptions, LocalSkillRemovalResult, McpRootJson, McpServerConfig, McpTarget, OAuthCallbackPayload, OAuthExchangeResponse, OAuthLoopbackListener, OAuthStartResponse, RunOAuthBrowserLoginParams, SkillSummary, SyncOptions, SyncResult, } from './types.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { isMainModule, main } from './app.js';
|
|
3
|
+
export { ConfigStore, loadConfig, saveConfig } from './config.js';
|
|
4
|
+
export { cmdLogin, getUsageText, isMainModule, loadCliLanguage, main, parseCliLanguage, saveCliLanguage, InvokoraCliApp } from './app.js';
|
|
5
|
+
export { base64UrlEncode, buildS256CodeChallenge, chooseLoopbackHost, createOAuthLoopbackListener, exchangeOAuthCode, generateCodeVerifier, generateOAuthState, normalizeCallbackError, OAuthBrowserLoginFlow, OAuthCLIClient, OAuthLoopbackServer, openBrowserURL, parseLoginCommandOptions, runOAuthBrowserLogin, startOAuthCLI, withTimeout, } from './oauth.js';
|
|
6
|
+
if (isMainModule(import.meta.url)) {
|
|
7
|
+
main().catch((error) => {
|
|
8
|
+
console.error('Error:', error instanceof Error ? error.message : String(error));
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { type HandleChatToolOptions, type StartStdioServerOptions } from './schemas.js';
|
|
3
|
+
import type { ReasoningEffort } from '../types.js';
|
|
4
|
+
export declare class InvokoraMcpApp {
|
|
5
|
+
private readonly options;
|
|
6
|
+
constructor(options: HandleChatToolOptions);
|
|
7
|
+
createServer(): McpServer;
|
|
8
|
+
}
|
|
9
|
+
export declare function createMcpServer(options: HandleChatToolOptions): McpServer;
|
|
10
|
+
export declare function resolveBackendBaseUrl(options?: Partial<StartStdioServerOptions>): string;
|
|
11
|
+
export declare function startStdioServer(options?: Partial<StartStdioServerOptions>): Promise<McpServer>;
|
|
12
|
+
export declare function normalizeReasoningEffort(value: unknown): ReasoningEffort | undefined;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { loadConfig } from '../config.js';
|
|
4
|
+
import { DEFAULT_BACKEND_URL, RELEASE_VERSION } from '../release.js';
|
|
5
|
+
import { SKILZ_CHAT_TOOL, ChatToolInputSchema, ChatToolOutputSchema, } from './schemas.js';
|
|
6
|
+
import { handleChatTool } from './tool_handlers.js';
|
|
7
|
+
import { createProgressReporter } from './progress.js';
|
|
8
|
+
const SERVER_INSTRUCTIONS = [
|
|
9
|
+
'Use chat for every Invokora Skill request. Put the user intent and slash command, such as /code-review, in messages.',
|
|
10
|
+
'Invokora injects hosted Skill content server-side. This MCP server never exposes raw hosted Skill source content.',
|
|
11
|
+
'This MCP server does not read files, write files, run shell commands, or grant local tools. The host agent decides whether to collect local context and include it in chat messages.',
|
|
12
|
+
'If a hosted Skill asks for local context or a patch, it returns structured fenced JSON for the host to evaluate; those blocks are not MCP tool calls.',
|
|
13
|
+
'This server is local stdio MCP only. Remote HTTP MCP is not currently provided.',
|
|
14
|
+
].join('\n');
|
|
15
|
+
export class InvokoraMcpApp {
|
|
16
|
+
options;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
createServer() {
|
|
21
|
+
const server = new McpServer({
|
|
22
|
+
name: 'invokora',
|
|
23
|
+
version: RELEASE_VERSION,
|
|
24
|
+
}, { instructions: SERVER_INSTRUCTIONS });
|
|
25
|
+
server.registerTool(SKILZ_CHAT_TOOL, {
|
|
26
|
+
title: 'Chat with Invokora',
|
|
27
|
+
description: 'Send an Invokora chat turn through the unified Responses API. ' +
|
|
28
|
+
'Include a slash command such as /code-review in the user message to select or switch Skills; later turns may omit it and continue the local session. ' +
|
|
29
|
+
'The host agent, not this MCP server, collects local file context and decides whether to apply any suggested actions.',
|
|
30
|
+
inputSchema: ChatToolInputSchema,
|
|
31
|
+
outputSchema: ChatToolOutputSchema,
|
|
32
|
+
annotations: {
|
|
33
|
+
readOnlyHint: false,
|
|
34
|
+
destructiveHint: true,
|
|
35
|
+
idempotentHint: false,
|
|
36
|
+
openWorldHint: true,
|
|
37
|
+
},
|
|
38
|
+
}, async (input, extra) => handleChatTool(input, {
|
|
39
|
+
...this.options,
|
|
40
|
+
progressReporter: createProgressReporter(extra._meta?.progressToken, extra.sendNotification),
|
|
41
|
+
signal: extra.signal,
|
|
42
|
+
}));
|
|
43
|
+
return server;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function createMcpServer(options) {
|
|
47
|
+
return new InvokoraMcpApp(options).createServer();
|
|
48
|
+
}
|
|
49
|
+
export function resolveBackendBaseUrl(options = {}) {
|
|
50
|
+
return options.backendBaseUrl ?? process.env.SKILZ_BACKEND_URL ?? DEFAULT_BACKEND_URL;
|
|
51
|
+
}
|
|
52
|
+
export async function startStdioServer(options = {}) {
|
|
53
|
+
const localConfig = loadConfig();
|
|
54
|
+
const backendBaseUrl = options.backendBaseUrl ?? process.env.SKILZ_BACKEND_URL ?? localConfig?.backend_url ?? DEFAULT_BACKEND_URL;
|
|
55
|
+
const authToken = options.authToken ?? process.env.SKILZ_TOKEN ?? localConfig?.token;
|
|
56
|
+
const refreshToken = options.refreshToken ?? process.env.SKILZ_REFRESH_TOKEN ?? localConfig?.refresh_token;
|
|
57
|
+
const apiKey = options.apiKey ?? process.env.SKILZ_API_KEY;
|
|
58
|
+
const defaultModel = options.defaultModel ?? process.env.SKILZ_DEFAULT_MODEL ?? localConfig?.default_model;
|
|
59
|
+
const defaultReason = normalizeReasoningEffort(options.defaultReason ?? process.env.SKILZ_DEFAULT_REASON ?? localConfig?.default_reason);
|
|
60
|
+
const server = createMcpServer({
|
|
61
|
+
backendBaseUrl,
|
|
62
|
+
authToken,
|
|
63
|
+
refreshToken,
|
|
64
|
+
apiKey,
|
|
65
|
+
defaultModel,
|
|
66
|
+
defaultReason,
|
|
67
|
+
fetchImpl: options.fetchImpl,
|
|
68
|
+
});
|
|
69
|
+
const transport = new StdioServerTransport();
|
|
70
|
+
await server.connect(transport);
|
|
71
|
+
return server;
|
|
72
|
+
}
|
|
73
|
+
export function normalizeReasoningEffort(value) {
|
|
74
|
+
if (typeof value !== 'string') {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const normalized = value.trim().toLowerCase();
|
|
78
|
+
if (!normalized) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
if (normalized === 'low' || normalized === 'medium' || normalized === 'high' || normalized === 'xhigh') {
|
|
82
|
+
return normalized;
|
|
83
|
+
}
|
|
84
|
+
throw new Error('default reason must be one of: low, medium, high, xhigh');
|
|
85
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ChatToolInput, ChatToolOutput, ChatViaBackendOptions } from './schemas.js';
|
|
2
|
+
export declare class InvokoraBackendClient {
|
|
3
|
+
private readonly options;
|
|
4
|
+
private readonly fetchImpl;
|
|
5
|
+
constructor(options: ChatViaBackendOptions);
|
|
6
|
+
chat(input: ChatToolInput): Promise<ChatToolOutput>;
|
|
7
|
+
private runChatSession;
|
|
8
|
+
private responsesBearerToken;
|
|
9
|
+
}
|
|
10
|
+
export declare function refreshAccessToken(backendBaseUrl: string, refreshToken: string, fetchImpl?: typeof fetch): Promise<string>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { runResponsesChatSession } from './responses_session.js';
|
|
2
|
+
import { BackendInvocationError, createBackendInvocationError, formatHttpResponseDiagnostic, getErrorMessage, parseJsonResponse, } from './errors.js';
|
|
3
|
+
export class InvokoraBackendClient {
|
|
4
|
+
options;
|
|
5
|
+
fetchImpl;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
9
|
+
}
|
|
10
|
+
async chat(input) {
|
|
11
|
+
const bearerToken = await this.responsesBearerToken();
|
|
12
|
+
try {
|
|
13
|
+
return await this.runChatSession(input, bearerToken);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error instanceof BackendInvocationError && error.status === 401 && !this.options.apiKey && this.options.authToken && this.options.refreshToken) {
|
|
17
|
+
const refreshed = await refreshAccessToken(this.options.backendBaseUrl, this.options.refreshToken, this.fetchImpl);
|
|
18
|
+
try {
|
|
19
|
+
return await this.runChatSession(input, refreshed);
|
|
20
|
+
}
|
|
21
|
+
catch (retryError) {
|
|
22
|
+
throw withSupportHint(retryError, this.options.backendBaseUrl, input.session_id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
throw withSupportHint(error, this.options.backendBaseUrl, input.session_id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async runChatSession(input, bearerToken) {
|
|
29
|
+
return runResponsesChatSession({
|
|
30
|
+
backendBaseUrl: this.options.backendBaseUrl,
|
|
31
|
+
apiKey: bearerToken,
|
|
32
|
+
model: input.model,
|
|
33
|
+
reason: input.reason,
|
|
34
|
+
defaultModel: this.options.defaultModel,
|
|
35
|
+
defaultReason: this.options.defaultReason,
|
|
36
|
+
sessionID: input.session_id,
|
|
37
|
+
messages: input.messages,
|
|
38
|
+
sessionStore: this.options.sessionStore,
|
|
39
|
+
compactAfterChars: this.options.compactAfterChars,
|
|
40
|
+
fetchImpl: this.options.fetchImpl,
|
|
41
|
+
progressReporter: this.options.progressReporter,
|
|
42
|
+
signal: this.options.signal,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async responsesBearerToken() {
|
|
46
|
+
if (this.options.apiKey) {
|
|
47
|
+
return this.options.apiKey;
|
|
48
|
+
}
|
|
49
|
+
if (this.options.authToken) {
|
|
50
|
+
return this.options.authToken;
|
|
51
|
+
}
|
|
52
|
+
throw new Error('Run: invokora login');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function withSupportHint(error, backendBaseUrl, sessionId) {
|
|
56
|
+
const base = error instanceof Error ? error : new Error(String(error));
|
|
57
|
+
const q = new URLSearchParams();
|
|
58
|
+
if (sessionId)
|
|
59
|
+
q.set('session_id', sessionId);
|
|
60
|
+
q.set('category', 'cannot_invoke');
|
|
61
|
+
const code = error?.code;
|
|
62
|
+
if (code)
|
|
63
|
+
q.set('last_error_code', code);
|
|
64
|
+
q.set('last_error_message', base.message.slice(0, 500));
|
|
65
|
+
const supportUrl = new URL(`/workspace/support?${q.toString()}`, backendBaseUrl).toString();
|
|
66
|
+
return new Error(`${base.message}\nSupport: ${supportUrl}`);
|
|
67
|
+
}
|
|
68
|
+
export async function refreshAccessToken(backendBaseUrl, refreshToken, fetchImpl = fetch) {
|
|
69
|
+
const url = new URL('/v1/auth/refresh', backendBaseUrl);
|
|
70
|
+
let response;
|
|
71
|
+
try {
|
|
72
|
+
response = await fetchImpl(url.toString(), {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'content-type': 'application/json' },
|
|
75
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
throw new Error(`Failed to reach backend: ${getErrorMessage(error)}`);
|
|
80
|
+
}
|
|
81
|
+
const responseBody = await parseJsonResponse(response);
|
|
82
|
+
const body = responseBody;
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw createBackendInvocationError(response.status, responseBody);
|
|
85
|
+
}
|
|
86
|
+
const token = body?.token;
|
|
87
|
+
if (!token) {
|
|
88
|
+
throw new Error(`Token refresh response missing token field: ${formatHttpResponseDiagnostic(response.status, responseBody)}`);
|
|
89
|
+
}
|
|
90
|
+
return token;
|
|
91
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type ToolResult, type ToolTextContent } from './schemas.js';
|
|
3
|
+
export declare class BackendInvocationError extends Error {
|
|
4
|
+
readonly code?: string;
|
|
5
|
+
readonly status: number;
|
|
6
|
+
readonly requestId?: string;
|
|
7
|
+
constructor(params: {
|
|
8
|
+
status: number;
|
|
9
|
+
message: string;
|
|
10
|
+
code?: string;
|
|
11
|
+
requestId?: string;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export declare function createErrorResult<TStructured extends Record<string, unknown> = Record<string, unknown>>(message: string, structuredContent?: TStructured): ToolResult<TStructured>;
|
|
15
|
+
export declare function formatToolError(error: unknown): string;
|
|
16
|
+
export declare function formatZodError(error: z.ZodError): string;
|
|
17
|
+
export declare function getErrorMessage(error: unknown): string;
|
|
18
|
+
export declare function parseJsonResponse(response: Response): Promise<unknown>;
|
|
19
|
+
export declare function summarizeResponseBody(responseBody: unknown): string;
|
|
20
|
+
export declare function formatHttpResponseDiagnostic(status: number, responseBody: unknown): string;
|
|
21
|
+
export declare function createBackendInvocationError(status: number, responseBody: unknown): BackendInvocationError;
|
|
22
|
+
export declare function createBackendInvocationErrorFromPayload(status: number, payload: {
|
|
23
|
+
code?: unknown;
|
|
24
|
+
message?: unknown;
|
|
25
|
+
}): BackendInvocationError;
|
|
26
|
+
export declare function textContent(text: string): {
|
|
27
|
+
content: ToolTextContent[];
|
|
28
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { BackendErrorResponseSchema } from './schemas.js';
|
|
2
|
+
const RESPONSE_BODY_SNIPPET_LIMIT = 200;
|
|
3
|
+
const KNOWN_BACKEND_ERROR_MESSAGES = {
|
|
4
|
+
BALANCE_EXHAUSTED: 'Wallet balance is too low. Top up your Invokora wallet before continuing.',
|
|
5
|
+
INSUFFICIENT_WALLET_BALANCE: 'Wallet balance is too low. Top up your Invokora wallet before continuing.',
|
|
6
|
+
'balance_exhausted': 'Wallet balance is too low. Top up your Invokora wallet before continuing.',
|
|
7
|
+
'error.insufficient_wallet_balance': 'Wallet balance is too low. Top up your Invokora wallet before continuing.',
|
|
8
|
+
};
|
|
9
|
+
export class BackendInvocationError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
status;
|
|
12
|
+
requestId;
|
|
13
|
+
constructor(params) {
|
|
14
|
+
super(params.message);
|
|
15
|
+
this.name = 'BackendInvocationError';
|
|
16
|
+
this.status = params.status;
|
|
17
|
+
this.code = params.code;
|
|
18
|
+
this.requestId = params.requestId;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function createErrorResult(message, structuredContent) {
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: 'text', text: message }],
|
|
24
|
+
...(structuredContent ? { structuredContent } : {}),
|
|
25
|
+
isError: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function formatToolError(error) {
|
|
29
|
+
if (error instanceof BackendInvocationError) {
|
|
30
|
+
return error.message;
|
|
31
|
+
}
|
|
32
|
+
return getErrorMessage(error);
|
|
33
|
+
}
|
|
34
|
+
export function formatZodError(error) {
|
|
35
|
+
return error.issues
|
|
36
|
+
.map((issue) => {
|
|
37
|
+
const path = issue.path.join('.');
|
|
38
|
+
return path.length > 0 ? `${path}: ${issue.message}` : issue.message;
|
|
39
|
+
})
|
|
40
|
+
.join('; ');
|
|
41
|
+
}
|
|
42
|
+
export function getErrorMessage(error) {
|
|
43
|
+
return error instanceof Error ? error.message : String(error);
|
|
44
|
+
}
|
|
45
|
+
export async function parseJsonResponse(response) {
|
|
46
|
+
const bodyText = await response.text();
|
|
47
|
+
if (bodyText.length === 0)
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(bodyText);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return bodyText;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function summarizeResponseBody(responseBody) {
|
|
57
|
+
if (responseBody === undefined) {
|
|
58
|
+
return 'empty response body';
|
|
59
|
+
}
|
|
60
|
+
const raw = typeof responseBody === 'string'
|
|
61
|
+
? responseBody
|
|
62
|
+
: (() => {
|
|
63
|
+
try {
|
|
64
|
+
return JSON.stringify(responseBody);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return String(responseBody);
|
|
68
|
+
}
|
|
69
|
+
})();
|
|
70
|
+
const normalized = raw.replace(/\s+/g, ' ').trim();
|
|
71
|
+
if (normalized.length === 0) {
|
|
72
|
+
return 'empty response body';
|
|
73
|
+
}
|
|
74
|
+
if (normalized.length <= RESPONSE_BODY_SNIPPET_LIMIT) {
|
|
75
|
+
return normalized;
|
|
76
|
+
}
|
|
77
|
+
return `${normalized.slice(0, RESPONSE_BODY_SNIPPET_LIMIT)}...`;
|
|
78
|
+
}
|
|
79
|
+
export function formatHttpResponseDiagnostic(status, responseBody) {
|
|
80
|
+
const parsedError = BackendErrorResponseSchema.safeParse(responseBody);
|
|
81
|
+
if (parsedError.success) {
|
|
82
|
+
const readableMessage = formatKnownBackendErrorMessage(parsedError.data.error);
|
|
83
|
+
if (readableMessage) {
|
|
84
|
+
const codeSuffix = parsedError.data.error.code ? ` (${parsedError.data.error.code})` : '';
|
|
85
|
+
const requestIdSuffix = parsedError.data.request_id ? ` request_id=${parsedError.data.request_id}` : '';
|
|
86
|
+
return `${readableMessage}${codeSuffix}${requestIdSuffix}`;
|
|
87
|
+
}
|
|
88
|
+
const requestIdSuffix = parsedError.data.request_id ? ` request_id=${parsedError.data.request_id}` : '';
|
|
89
|
+
return `HTTP ${status} ${parsedError.data.error.code}: ${parsedError.data.error.message}${requestIdSuffix}`;
|
|
90
|
+
}
|
|
91
|
+
return `HTTP ${status}; response=${summarizeResponseBody(responseBody)}`;
|
|
92
|
+
}
|
|
93
|
+
export function createBackendInvocationError(status, responseBody) {
|
|
94
|
+
const parsedError = BackendErrorResponseSchema.safeParse(responseBody);
|
|
95
|
+
const diagnostic = formatHttpResponseDiagnostic(status, responseBody);
|
|
96
|
+
if (parsedError.success) {
|
|
97
|
+
return new BackendInvocationError({
|
|
98
|
+
status,
|
|
99
|
+
code: parsedError.data.error.code,
|
|
100
|
+
message: diagnostic,
|
|
101
|
+
requestId: parsedError.data.request_id,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return new BackendInvocationError({
|
|
105
|
+
status,
|
|
106
|
+
message: diagnostic,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export function createBackendInvocationErrorFromPayload(status, payload) {
|
|
110
|
+
const code = normalizeBackendErrorToken(payload.code);
|
|
111
|
+
const message = normalizeBackendErrorToken(payload.message);
|
|
112
|
+
const readableMessage = formatKnownBackendErrorMessage({ code, message });
|
|
113
|
+
return new BackendInvocationError({
|
|
114
|
+
status,
|
|
115
|
+
message: readableMessage ?? message ?? (code ? `Backend returned ${code}` : 'Backend invocation failed'),
|
|
116
|
+
...(code ? { code } : {}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function formatKnownBackendErrorMessage(error) {
|
|
120
|
+
const code = normalizeBackendErrorToken(error.code);
|
|
121
|
+
if (code && KNOWN_BACKEND_ERROR_MESSAGES[code]) {
|
|
122
|
+
return KNOWN_BACKEND_ERROR_MESSAGES[code];
|
|
123
|
+
}
|
|
124
|
+
const message = normalizeBackendErrorToken(error.message);
|
|
125
|
+
if (message && KNOWN_BACKEND_ERROR_MESSAGES[message]) {
|
|
126
|
+
return KNOWN_BACKEND_ERROR_MESSAGES[message];
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
function normalizeBackendErrorToken(value) {
|
|
131
|
+
if (typeof value !== 'string') {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const trimmed = value.trim();
|
|
135
|
+
return trimmed ? trimmed : undefined;
|
|
136
|
+
}
|
|
137
|
+
export function textContent(text) {
|
|
138
|
+
return { content: [{ type: 'text', text }] };
|
|
139
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProgressToken, ServerNotification } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export type ProgressUpdate = {
|
|
3
|
+
progress: number;
|
|
4
|
+
total?: number;
|
|
5
|
+
message?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ProgressReporter = {
|
|
8
|
+
report(update: ProgressUpdate): Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
type SendNotification = (notification: ServerNotification) => Promise<void>;
|
|
11
|
+
export declare function createProgressReporter(progressToken: ProgressToken | undefined, sendNotification: SendNotification): ProgressReporter | undefined;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function createProgressReporter(progressToken, sendNotification) {
|
|
2
|
+
if (progressToken === undefined) {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
let disabled = false;
|
|
6
|
+
let lastProgress = Number.NEGATIVE_INFINITY;
|
|
7
|
+
return {
|
|
8
|
+
async report(update) {
|
|
9
|
+
if (disabled) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const progress = nextMonotonicProgress(update.progress, update.total, lastProgress);
|
|
13
|
+
if (progress === undefined) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
lastProgress = progress;
|
|
17
|
+
try {
|
|
18
|
+
await sendNotification({
|
|
19
|
+
method: 'notifications/progress',
|
|
20
|
+
params: {
|
|
21
|
+
progressToken,
|
|
22
|
+
progress,
|
|
23
|
+
...(update.total !== undefined ? { total: update.total } : {}),
|
|
24
|
+
...(update.message ? { message: update.message } : {}),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
disabled = true;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function nextMonotonicProgress(progress, total, lastProgress) {
|
|
35
|
+
if (!Number.isFinite(progress)) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
let next = progress;
|
|
39
|
+
if (next <= lastProgress) {
|
|
40
|
+
next = lastProgress + 0.001;
|
|
41
|
+
}
|
|
42
|
+
if (total !== undefined && next > total) {
|
|
43
|
+
if (lastProgress >= total) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
next = total;
|
|
47
|
+
}
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { McpSessionStore, type ChatMessage } from './session_store.js';
|
|
2
|
+
import type { ChatToolOutput } from './schemas.js';
|
|
3
|
+
import type { ProgressReporter } from './progress.js';
|
|
4
|
+
import type { ReasoningEffort } from '../types.js';
|
|
5
|
+
type ResponsesChatSessionOptions = {
|
|
6
|
+
backendBaseUrl: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
messages: ChatMessage[];
|
|
9
|
+
sessionID?: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
reason?: ReasoningEffort;
|
|
12
|
+
defaultModel?: string;
|
|
13
|
+
defaultReason?: ReasoningEffort;
|
|
14
|
+
sessionStore?: McpSessionStore;
|
|
15
|
+
fetchImpl?: typeof fetch;
|
|
16
|
+
compactAfterChars?: number;
|
|
17
|
+
progressReporter?: ProgressReporter;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
};
|
|
20
|
+
export declare function runResponsesChatSession(options: ResponsesChatSessionOptions): Promise<ChatToolOutput>;
|
|
21
|
+
export {};
|