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,233 @@
|
|
|
1
|
+
import { createBackendInvocationError, createBackendInvocationErrorFromPayload, parseJsonResponse } from './errors.js';
|
|
2
|
+
import { McpSessionStore, replayInputForSession } from './session_store.js';
|
|
3
|
+
const COMPACT_CONTEXT_WINDOW_TOKENS = 400_000;
|
|
4
|
+
const COMPACT_MAX_OUTPUT_TOKENS = 128_000;
|
|
5
|
+
const COMPACT_REPLAY_BUDGET_TOKENS = COMPACT_CONTEXT_WINDOW_TOKENS - COMPACT_MAX_OUTPUT_TOKENS;
|
|
6
|
+
const APPROX_CHARS_PER_TOKEN = 4;
|
|
7
|
+
const DEFAULT_COMPACT_AFTER_CHARS = COMPACT_REPLAY_BUDGET_TOKENS * APPROX_CHARS_PER_TOKEN;
|
|
8
|
+
export async function runResponsesChatSession(options) {
|
|
9
|
+
await reportProgress(options.progressReporter, 10, 'Preparing MCP session...');
|
|
10
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
11
|
+
const store = options.sessionStore ?? new McpSessionStore();
|
|
12
|
+
const session = store.loadOrCreate(options.sessionID);
|
|
13
|
+
const appendedMessages = normalizeMessages(options.messages);
|
|
14
|
+
if (options.model?.trim()) {
|
|
15
|
+
session.model = options.model.trim();
|
|
16
|
+
}
|
|
17
|
+
else if (!session.model && options.defaultModel?.trim()) {
|
|
18
|
+
session.model = options.defaultModel.trim();
|
|
19
|
+
}
|
|
20
|
+
if (options.reason) {
|
|
21
|
+
session.reason = options.reason;
|
|
22
|
+
}
|
|
23
|
+
else if (!session.reason && options.defaultReason) {
|
|
24
|
+
session.reason = options.defaultReason;
|
|
25
|
+
}
|
|
26
|
+
session.metadata = { client_surface: 'mcp', session_id: session.session_id };
|
|
27
|
+
await compactSessionIfNeeded(fetchImpl, options, session);
|
|
28
|
+
session.messages.push(...appendedMessages);
|
|
29
|
+
session.updated_at = new Date().toISOString();
|
|
30
|
+
const payload = buildResponsesPayload(session, replayInputForSession(session));
|
|
31
|
+
await reportProgress(options.progressReporter, 35, 'Calling Invokora Responses...');
|
|
32
|
+
const response = await postResponses(fetchImpl, options.backendBaseUrl, options.apiKey, payload, options);
|
|
33
|
+
if (response.output_text && response.output_text.trim()) {
|
|
34
|
+
session.messages.push({ role: 'assistant', content: response.output_text });
|
|
35
|
+
}
|
|
36
|
+
session.last_response_id = response.id;
|
|
37
|
+
session.model = response.model ?? session.model;
|
|
38
|
+
session.updated_at = new Date().toISOString();
|
|
39
|
+
await reportProgress(options.progressReporter, 95, 'Saving MCP session...');
|
|
40
|
+
store.save(session);
|
|
41
|
+
await reportProgress(options.progressReporter, 100, 'Done.');
|
|
42
|
+
return {
|
|
43
|
+
session_id: session.session_id,
|
|
44
|
+
response_id: response.id,
|
|
45
|
+
model: response.model ?? session.model ?? '',
|
|
46
|
+
output_text: response.output_text ?? '',
|
|
47
|
+
...(response.usage ? { usage: response.usage } : {}),
|
|
48
|
+
...(response.charge ? { charge: response.charge } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function normalizeMessages(messages) {
|
|
52
|
+
if (messages.length === 0) {
|
|
53
|
+
throw new Error('messages must not be empty');
|
|
54
|
+
}
|
|
55
|
+
const normalized = messages.map((message) => ({
|
|
56
|
+
role: message.role,
|
|
57
|
+
content: message.content,
|
|
58
|
+
}));
|
|
59
|
+
const last = normalized[normalized.length - 1];
|
|
60
|
+
if (last.role !== 'user' || last.content.trim() === '') {
|
|
61
|
+
throw new Error('messages must end with a non-empty user message');
|
|
62
|
+
}
|
|
63
|
+
for (const message of normalized) {
|
|
64
|
+
if ((message.role !== 'user' && message.role !== 'assistant') || message.content.trim() === '') {
|
|
65
|
+
throw new Error('messages only support non-empty user and assistant text');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|
|
70
|
+
function buildResponsesPayload(session, input) {
|
|
71
|
+
return {
|
|
72
|
+
...(session.model ? { model: session.model } : {}),
|
|
73
|
+
...(session.reason ? { reasoning: { effort: session.reason } } : {}),
|
|
74
|
+
input,
|
|
75
|
+
stream: true,
|
|
76
|
+
metadata: { client_surface: 'mcp', session_id: session.session_id },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function compactSessionIfNeeded(fetchImpl, options, session) {
|
|
80
|
+
const compactAfterChars = options.compactAfterChars ?? DEFAULT_COMPACT_AFTER_CHARS;
|
|
81
|
+
const replayInput = replayInputForSession(session);
|
|
82
|
+
if (JSON.stringify(replayInput).length < compactAfterChars || replayInput.length < 4) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await reportProgress(options.progressReporter, 20, 'Compacting long MCP session...');
|
|
87
|
+
const item = await postCompact(fetchImpl, options.backendBaseUrl, options.apiKey, {
|
|
88
|
+
...(session.model ? { model: session.model } : {}),
|
|
89
|
+
input: replayInput,
|
|
90
|
+
metadata: { client_surface: 'mcp', session_id: session.session_id },
|
|
91
|
+
}, options.signal);
|
|
92
|
+
session.compacted_item = item;
|
|
93
|
+
session.messages = [];
|
|
94
|
+
session.last_response_id = undefined;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (options.signal?.aborted) {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
// 压缩失败不能静默丢上下文;保留完整 session,后续 request body 超限时由后端暴露明确错误。
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function postResponses(fetchImpl, backendBaseUrl, apiKey, payload, options) {
|
|
104
|
+
const url = new URL('/v1/responses', backendBaseUrl);
|
|
105
|
+
const response = await fetchImpl(url.toString(), {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: {
|
|
108
|
+
'content-type': 'application/json',
|
|
109
|
+
authorization: `Bearer ${apiKey}`,
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify(payload),
|
|
112
|
+
signal: options.signal,
|
|
113
|
+
});
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw createBackendInvocationError(response.status, await parseJsonResponse(response));
|
|
116
|
+
}
|
|
117
|
+
if ((response.headers.get('content-type') ?? '').includes('text/event-stream')) {
|
|
118
|
+
return parseResponsesSSE(response, response.status, options.progressReporter);
|
|
119
|
+
}
|
|
120
|
+
return (await parseJsonResponse(response));
|
|
121
|
+
}
|
|
122
|
+
async function postCompact(fetchImpl, backendBaseUrl, apiKey, payload, signal) {
|
|
123
|
+
const url = new URL('/v1/responses/compact', backendBaseUrl);
|
|
124
|
+
const response = await fetchImpl(url.toString(), {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: {
|
|
127
|
+
'content-type': 'application/json',
|
|
128
|
+
authorization: `Bearer ${apiKey}`,
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify(payload),
|
|
131
|
+
signal,
|
|
132
|
+
});
|
|
133
|
+
const responseBody = await parseJsonResponse(response);
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
throw createBackendInvocationError(response.status, responseBody);
|
|
136
|
+
}
|
|
137
|
+
const body = responseBody;
|
|
138
|
+
const direct = body && body.type === 'compaction' ? body : undefined;
|
|
139
|
+
const fromField = body?.compaction;
|
|
140
|
+
const fromOutput = body?.output?.find((item) => item.type === 'compaction');
|
|
141
|
+
const item = direct ?? fromField ?? fromOutput;
|
|
142
|
+
if (!item) {
|
|
143
|
+
throw new Error('compact response did not include a compaction item');
|
|
144
|
+
}
|
|
145
|
+
return item;
|
|
146
|
+
}
|
|
147
|
+
async function parseResponsesSSE(response, status, progressReporter) {
|
|
148
|
+
if (!response.body) {
|
|
149
|
+
return parseResponsesSSEText(await response.text(), status, progressReporter);
|
|
150
|
+
}
|
|
151
|
+
const reader = response.body.getReader();
|
|
152
|
+
const decoder = new TextDecoder();
|
|
153
|
+
const state = { generating: false, deltaEvents: 0 };
|
|
154
|
+
let buffer = '';
|
|
155
|
+
while (true) {
|
|
156
|
+
const { done, value } = await reader.read();
|
|
157
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
158
|
+
buffer = buffer.replace(/\r\n/g, '\n');
|
|
159
|
+
let boundary = buffer.indexOf('\n\n');
|
|
160
|
+
while (boundary >= 0) {
|
|
161
|
+
const event = buffer.slice(0, boundary);
|
|
162
|
+
buffer = buffer.slice(boundary + 2);
|
|
163
|
+
await processResponsesSSEEvent(event, status, state, progressReporter);
|
|
164
|
+
boundary = buffer.indexOf('\n\n');
|
|
165
|
+
}
|
|
166
|
+
if (done) {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const remaining = buffer.trim();
|
|
171
|
+
if (remaining) {
|
|
172
|
+
await processResponsesSSEEvent(remaining, status, state, progressReporter);
|
|
173
|
+
}
|
|
174
|
+
return completedResponseOrThrow(state.completed, status);
|
|
175
|
+
}
|
|
176
|
+
async function parseResponsesSSEText(text, status, progressReporter) {
|
|
177
|
+
const state = { generating: false, deltaEvents: 0 };
|
|
178
|
+
for (const event of text.replace(/\r\n/g, '\n').split(/\n\n+/)) {
|
|
179
|
+
await processResponsesSSEEvent(event, status, state, progressReporter);
|
|
180
|
+
}
|
|
181
|
+
return completedResponseOrThrow(state.completed, status);
|
|
182
|
+
}
|
|
183
|
+
async function processResponsesSSEEvent(event, status, state, progressReporter) {
|
|
184
|
+
const payload = event
|
|
185
|
+
.split('\n')
|
|
186
|
+
.map((line) => line.trim())
|
|
187
|
+
.filter((line) => line.startsWith('data:'))
|
|
188
|
+
.map((line) => line.slice('data:'.length).trim())
|
|
189
|
+
.join('\n')
|
|
190
|
+
.trim();
|
|
191
|
+
if (!payload || payload === '[DONE]') {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
let data;
|
|
195
|
+
try {
|
|
196
|
+
data = JSON.parse(payload);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (data.type === 'error' || data.type === 'response.failed' || data.error) {
|
|
202
|
+
throw createBackendInvocationErrorFromPayload(status, data.error ?? { message: data.message ?? 'Responses SSE error' });
|
|
203
|
+
}
|
|
204
|
+
if (data.type === 'response.output_text.delta') {
|
|
205
|
+
state.deltaEvents += 1;
|
|
206
|
+
if (!state.generating) {
|
|
207
|
+
state.generating = true;
|
|
208
|
+
await reportProgress(progressReporter, 45, 'Invokora is generating a response...');
|
|
209
|
+
}
|
|
210
|
+
else if (state.deltaEvents % 25 === 0) {
|
|
211
|
+
await reportProgress(progressReporter, Math.min(90, 45 + Math.floor(state.deltaEvents / 25)), 'Still generating...');
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (data.type === 'response.completed' && data.response) {
|
|
216
|
+
state.completed = data.response;
|
|
217
|
+
await reportProgress(progressReporter, 90, 'Invokora response received.');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function completedResponseOrThrow(completed, status) {
|
|
221
|
+
if (!completed?.id) {
|
|
222
|
+
throw new Error(`Responses SSE stream ended without response.completed: HTTP ${status}`);
|
|
223
|
+
}
|
|
224
|
+
return completed;
|
|
225
|
+
}
|
|
226
|
+
async function reportProgress(progressReporter, progress, message) {
|
|
227
|
+
try {
|
|
228
|
+
await progressReporter?.report({ progress, total: 100, message });
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// progress 只是客户端体验信号,不能改变 chat tool 的业务结果。
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ReasoningEffort } from '../types.js';
|
|
3
|
+
import type { ProgressReporter } from './progress.js';
|
|
4
|
+
export declare const SKILZ_CHAT_TOOL = "chat";
|
|
5
|
+
export declare const ReasoningEffortSchema: z.ZodEnum<{
|
|
6
|
+
low: "low";
|
|
7
|
+
medium: "medium";
|
|
8
|
+
high: "high";
|
|
9
|
+
xhigh: "xhigh";
|
|
10
|
+
}>;
|
|
11
|
+
export type ChatReason = ReasoningEffort;
|
|
12
|
+
export declare const ChatToolInputSchema: z.ZodObject<{
|
|
13
|
+
session_id: z.ZodOptional<z.ZodString>;
|
|
14
|
+
model: z.ZodOptional<z.ZodString>;
|
|
15
|
+
reason: z.ZodOptional<z.ZodEnum<{
|
|
16
|
+
low: "low";
|
|
17
|
+
medium: "medium";
|
|
18
|
+
high: "high";
|
|
19
|
+
xhigh: "xhigh";
|
|
20
|
+
}>>;
|
|
21
|
+
messages: z.ZodArray<z.ZodObject<{
|
|
22
|
+
role: z.ZodEnum<{
|
|
23
|
+
user: "user";
|
|
24
|
+
assistant: "assistant";
|
|
25
|
+
}>;
|
|
26
|
+
content: z.ZodString;
|
|
27
|
+
}, z.core.$strip>>;
|
|
28
|
+
}, z.core.$strict>;
|
|
29
|
+
export declare const ChatToolOutputSchema: z.ZodObject<{
|
|
30
|
+
session_id: z.ZodString;
|
|
31
|
+
response_id: z.ZodOptional<z.ZodString>;
|
|
32
|
+
model: z.ZodString;
|
|
33
|
+
output_text: z.ZodString;
|
|
34
|
+
usage: z.ZodOptional<z.ZodObject<{
|
|
35
|
+
input_tokens: z.ZodNumber;
|
|
36
|
+
output_tokens: z.ZodNumber;
|
|
37
|
+
cache_read_input_tokens: z.ZodNumber;
|
|
38
|
+
}, z.core.$strip>>;
|
|
39
|
+
charge: z.ZodOptional<z.ZodObject<{
|
|
40
|
+
charge_ref: z.ZodOptional<z.ZodString>;
|
|
41
|
+
product_id: z.ZodOptional<z.ZodString>;
|
|
42
|
+
billing_mode: z.ZodOptional<z.ZodString>;
|
|
43
|
+
skill_fee_cents: z.ZodNumber;
|
|
44
|
+
skill_fee_microusd: z.ZodNumber;
|
|
45
|
+
model_fee_cents: z.ZodNumber;
|
|
46
|
+
model_fee_microcents: z.ZodOptional<z.ZodNumber>;
|
|
47
|
+
model_fee_microusd: z.ZodNumber;
|
|
48
|
+
total_fee_cents: z.ZodNumber;
|
|
49
|
+
total_fee_microusd: z.ZodNumber;
|
|
50
|
+
wallet_hold_cents: z.ZodNumber;
|
|
51
|
+
wallet_hold_microusd: z.ZodNumber;
|
|
52
|
+
charge_status: z.ZodOptional<z.ZodString>;
|
|
53
|
+
}, z.core.$strip>>;
|
|
54
|
+
}, z.core.$strict>;
|
|
55
|
+
export declare const BackendErrorResponseSchema: z.ZodObject<{
|
|
56
|
+
request_id: z.ZodOptional<z.ZodString>;
|
|
57
|
+
error: z.ZodObject<{
|
|
58
|
+
code: z.ZodString;
|
|
59
|
+
message: z.ZodString;
|
|
60
|
+
}, z.core.$strip>;
|
|
61
|
+
}, z.core.$strict>;
|
|
62
|
+
export type ChatToolInput = z.infer<typeof ChatToolInputSchema>;
|
|
63
|
+
export type ChatToolOutput = z.infer<typeof ChatToolOutputSchema>;
|
|
64
|
+
export type ToolTextContent = {
|
|
65
|
+
type: 'text';
|
|
66
|
+
text: string;
|
|
67
|
+
};
|
|
68
|
+
export type ToolResult<TStructured extends Record<string, unknown> = Record<string, unknown>> = {
|
|
69
|
+
content: ToolTextContent[];
|
|
70
|
+
structuredContent?: TStructured;
|
|
71
|
+
isError?: true;
|
|
72
|
+
};
|
|
73
|
+
export type ChatToolResult = ToolResult<ChatToolOutput>;
|
|
74
|
+
export type ResponsesCompleteResponse = {
|
|
75
|
+
id?: string;
|
|
76
|
+
model?: string;
|
|
77
|
+
output_text?: string;
|
|
78
|
+
usage?: ChatToolOutput['usage'];
|
|
79
|
+
charge?: ChatToolOutput['charge'];
|
|
80
|
+
error?: {
|
|
81
|
+
code?: string;
|
|
82
|
+
message?: string;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
export type ChatViaBackendOptions = {
|
|
86
|
+
backendBaseUrl: string;
|
|
87
|
+
authToken?: string;
|
|
88
|
+
refreshToken?: string;
|
|
89
|
+
apiKey?: string;
|
|
90
|
+
defaultModel?: string;
|
|
91
|
+
defaultReason?: ReasoningEffort;
|
|
92
|
+
fetchImpl?: typeof fetch;
|
|
93
|
+
sessionStore?: import('./session_store.js').McpSessionStore;
|
|
94
|
+
compactAfterChars?: number;
|
|
95
|
+
progressReporter?: ProgressReporter;
|
|
96
|
+
signal?: AbortSignal;
|
|
97
|
+
};
|
|
98
|
+
export type HandleChatToolOptions = ChatViaBackendOptions;
|
|
99
|
+
export type StartStdioServerOptions = ChatViaBackendOptions;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const SKILZ_CHAT_TOOL = 'chat';
|
|
3
|
+
export const ReasoningEffortSchema = z.enum(['low', 'medium', 'high', 'xhigh']);
|
|
4
|
+
const NonBlankStringSchema = z.string().refine((value) => value.trim().length > 0, {
|
|
5
|
+
message: 'Too small: expected string to have >=1 characters',
|
|
6
|
+
});
|
|
7
|
+
const MessageSchema = z.object({
|
|
8
|
+
role: z.enum(['user', 'assistant']).describe('Conversation role. The final message sent to chat must be user.'),
|
|
9
|
+
content: NonBlankStringSchema.describe('Plain text message content.'),
|
|
10
|
+
});
|
|
11
|
+
export const ChatToolInputSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
session_id: z
|
|
14
|
+
.string()
|
|
15
|
+
.trim()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Local MCP chat session id. Omit to create a new local session.'),
|
|
18
|
+
model: z.string().trim().optional().describe('Optional model override.'),
|
|
19
|
+
reason: ReasoningEffortSchema.optional().describe('Optional reasoning effort override: low, medium, high, or xhigh.'),
|
|
20
|
+
messages: z
|
|
21
|
+
.array(MessageSchema)
|
|
22
|
+
.min(1)
|
|
23
|
+
.describe('New plain-text user/assistant messages to append to the local MCP chat session. Last message must be role=user.'),
|
|
24
|
+
})
|
|
25
|
+
.strict();
|
|
26
|
+
export const ChatToolOutputSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
session_id: z.string(),
|
|
29
|
+
response_id: z.string().optional(),
|
|
30
|
+
model: z.string(),
|
|
31
|
+
output_text: z.string(),
|
|
32
|
+
usage: z
|
|
33
|
+
.object({
|
|
34
|
+
input_tokens: z.number().int(),
|
|
35
|
+
output_tokens: z.number().int(),
|
|
36
|
+
cache_read_input_tokens: z.number().int(),
|
|
37
|
+
})
|
|
38
|
+
.optional(),
|
|
39
|
+
charge: z
|
|
40
|
+
.object({
|
|
41
|
+
charge_ref: z.string().optional(),
|
|
42
|
+
product_id: z.string().optional(),
|
|
43
|
+
billing_mode: z.string().optional(),
|
|
44
|
+
skill_fee_cents: z.number().int(),
|
|
45
|
+
skill_fee_microusd: z.number().int(),
|
|
46
|
+
model_fee_cents: z.number().int(),
|
|
47
|
+
model_fee_microcents: z.number().int().optional(),
|
|
48
|
+
model_fee_microusd: z.number().int(),
|
|
49
|
+
total_fee_cents: z.number().int(),
|
|
50
|
+
total_fee_microusd: z.number().int(),
|
|
51
|
+
wallet_hold_cents: z.number().int(),
|
|
52
|
+
wallet_hold_microusd: z.number().int(),
|
|
53
|
+
charge_status: z.string().optional(),
|
|
54
|
+
})
|
|
55
|
+
.optional(),
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
export const BackendErrorResponseSchema = z
|
|
59
|
+
.object({
|
|
60
|
+
request_id: z.string().optional(),
|
|
61
|
+
error: z.object({
|
|
62
|
+
code: z.string(),
|
|
63
|
+
message: z.string(),
|
|
64
|
+
}),
|
|
65
|
+
})
|
|
66
|
+
.strict();
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { InvokoraMcpApp, createMcpServer, resolveBackendBaseUrl, startStdioServer } from './app.js';
|
|
2
|
+
export { chatViaBackend, handleChatTool, } from './tool_handlers.js';
|
|
3
|
+
export { BackendErrorResponseSchema, ChatToolInputSchema, ChatToolOutputSchema, SKILZ_CHAT_TOOL, } from './schemas.js';
|
|
4
|
+
export type { ChatToolInput, ChatToolOutput, ChatToolResult, ChatViaBackendOptions, HandleChatToolOptions, ResponsesCompleteResponse, StartStdioServerOptions, ToolTextContent, ToolResult, } from './schemas.js';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { InvokoraMcpApp, createMcpServer, resolveBackendBaseUrl, startStdioServer } from './app.js';
|
|
2
|
+
export { chatViaBackend, handleChatTool, } from './tool_handlers.js';
|
|
3
|
+
export { BackendErrorResponseSchema, ChatToolInputSchema, ChatToolOutputSchema, SKILZ_CHAT_TOOL, } from './schemas.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReasoningEffort } from '../types.js';
|
|
2
|
+
export type ChatMessage = {
|
|
3
|
+
role: 'user' | 'assistant';
|
|
4
|
+
content: string;
|
|
5
|
+
};
|
|
6
|
+
export type ResponsesInputItem = {
|
|
7
|
+
role: 'user' | 'assistant';
|
|
8
|
+
content: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: 'compaction';
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
};
|
|
13
|
+
export type McpChatSession = {
|
|
14
|
+
session_id: string;
|
|
15
|
+
messages: ChatMessage[];
|
|
16
|
+
compacted_item?: Record<string, unknown>;
|
|
17
|
+
last_response_id?: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
reason?: ReasoningEffort;
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
updated_at: string;
|
|
22
|
+
};
|
|
23
|
+
export declare class McpSessionStore {
|
|
24
|
+
private readonly baseDir;
|
|
25
|
+
constructor(baseDir?: string);
|
|
26
|
+
createSessionID(): string;
|
|
27
|
+
loadOrCreate(sessionID?: string): McpChatSession;
|
|
28
|
+
save(session: McpChatSession): void;
|
|
29
|
+
sessionPath(sessionID: string): string;
|
|
30
|
+
}
|
|
31
|
+
export declare function replayInputForSession(session: McpChatSession): ResponsesInputItem[];
|
|
32
|
+
export declare function validateSessionID(sessionID: string): void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { MCP_SESSION_DIR } from '../constants.js';
|
|
5
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9._-]{1,120}$/;
|
|
6
|
+
export class McpSessionStore {
|
|
7
|
+
baseDir;
|
|
8
|
+
constructor(baseDir = MCP_SESSION_DIR) {
|
|
9
|
+
this.baseDir = baseDir;
|
|
10
|
+
}
|
|
11
|
+
createSessionID() {
|
|
12
|
+
return randomUUID();
|
|
13
|
+
}
|
|
14
|
+
loadOrCreate(sessionID) {
|
|
15
|
+
const id = sessionID?.trim() || this.createSessionID();
|
|
16
|
+
validateSessionID(id);
|
|
17
|
+
const path = this.sessionPath(id);
|
|
18
|
+
if (!existsSync(path)) {
|
|
19
|
+
return { session_id: id, messages: [], updated_at: new Date(0).toISOString() };
|
|
20
|
+
}
|
|
21
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
22
|
+
if (parsed.session_id !== id || !Array.isArray(parsed.messages)) {
|
|
23
|
+
throw new Error(`Invalid MCP chat session: ${path}`);
|
|
24
|
+
}
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
save(session) {
|
|
28
|
+
validateSessionID(session.session_id);
|
|
29
|
+
const path = this.sessionPath(session.session_id);
|
|
30
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
31
|
+
const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
32
|
+
try {
|
|
33
|
+
writeFileSync(tmpPath, JSON.stringify(session, null, 2) + '\n', { mode: 0o600 });
|
|
34
|
+
renameSync(tmpPath, path);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
rmSync(tmpPath, { force: true });
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
sessionPath(sessionID) {
|
|
42
|
+
validateSessionID(sessionID);
|
|
43
|
+
return join(this.baseDir, `${sessionID}.json`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function replayInputForSession(session) {
|
|
47
|
+
const input = [];
|
|
48
|
+
if (session.compacted_item) {
|
|
49
|
+
input.push(session.compacted_item);
|
|
50
|
+
}
|
|
51
|
+
input.push(...session.messages.map((message) => ({ role: message.role, content: message.content })));
|
|
52
|
+
return input;
|
|
53
|
+
}
|
|
54
|
+
export function validateSessionID(sessionID) {
|
|
55
|
+
if (!SESSION_ID_PATTERN.test(sessionID)) {
|
|
56
|
+
throw new Error('session_id must be 1-120 chars and contain only letters, numbers, dot, underscore, or dash');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type ChatToolInput, type ChatToolOutput, type ChatToolResult, type HandleChatToolOptions } from './schemas.js';
|
|
2
|
+
export declare function handleChatTool(rawInput: unknown, options: HandleChatToolOptions): Promise<ChatToolResult>;
|
|
3
|
+
export declare function chatViaBackend(input: ChatToolInput, options: HandleChatToolOptions): Promise<ChatToolOutput>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createErrorResult, formatToolError, formatZodError } from './errors.js';
|
|
2
|
+
import { InvokoraBackendClient } from './backend_client.js';
|
|
3
|
+
import { ChatToolInputSchema, } from './schemas.js';
|
|
4
|
+
export async function handleChatTool(rawInput, options) {
|
|
5
|
+
const parsedInput = ChatToolInputSchema.safeParse(rawInput);
|
|
6
|
+
if (!parsedInput.success) {
|
|
7
|
+
return createErrorResult(`Invalid tool input: ${formatZodError(parsedInput.error)}`);
|
|
8
|
+
}
|
|
9
|
+
const lastMessage = parsedInput.data.messages[parsedInput.data.messages.length - 1];
|
|
10
|
+
if (lastMessage?.role !== 'user') {
|
|
11
|
+
return createErrorResult('Invalid tool input: messages must end with role=user');
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const structuredContent = await chatViaBackend(parsedInput.data, options);
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: 'text', text: structuredContent.output_text }],
|
|
17
|
+
structuredContent,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return createErrorResult(formatToolError(error));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function chatViaBackend(input, options) {
|
|
25
|
+
return new InvokoraBackendClient(options).chat(input);
|
|
26
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CliPrompts } from './prompts.js';
|
|
2
|
+
import type { Config, McpServerConfig, McpTarget, ParsedMcpSetupArgs } from './types.js';
|
|
3
|
+
export interface SetupTargetResult {
|
|
4
|
+
status: 'updated' | 'skipped';
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class McpConfigService {
|
|
8
|
+
private readonly prompts;
|
|
9
|
+
constructor(prompts: Pick<CliPrompts, 'pickMcpTargets' | 'yesNo'>);
|
|
10
|
+
buildServerConfig(_config: Config): McpServerConfig;
|
|
11
|
+
parseSetupArgs(args: string[]): ParsedMcpSetupArgs;
|
|
12
|
+
resolveTargets(parsed: ParsedMcpSetupArgs): Promise<McpTarget[]>;
|
|
13
|
+
mapPathsToTargets(targets: McpTarget[], paths: string[]): Map<McpTarget, string>;
|
|
14
|
+
setupTargetConfig(params: {
|
|
15
|
+
target: McpTarget;
|
|
16
|
+
explicitPath?: string;
|
|
17
|
+
serverConfig: McpServerConfig;
|
|
18
|
+
yes: boolean;
|
|
19
|
+
}): Promise<SetupTargetResult>;
|
|
20
|
+
private resolveClientConfigPath;
|
|
21
|
+
private writeJsonMcpConfig;
|
|
22
|
+
private loadOrInitMcpConfig;
|
|
23
|
+
private upsertInvokoraServer;
|
|
24
|
+
private writeCodexMcpConfig;
|
|
25
|
+
private writeSecureFile;
|
|
26
|
+
private resolveSecureFileMode;
|
|
27
|
+
private renderCodexInvokoraSection;
|
|
28
|
+
private escapeTomlString;
|
|
29
|
+
private isLikelyJson;
|
|
30
|
+
private upsertTomlSection;
|
|
31
|
+
private isMcpTarget;
|
|
32
|
+
private uniqueTargets;
|
|
33
|
+
}
|