feique 1.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/LICENSE +21 -0
- package/README.en.md +220 -0
- package/README.md +265 -0
- package/dist/backend/claude.d.ts +36 -0
- package/dist/backend/claude.js +358 -0
- package/dist/backend/claude.js.map +1 -0
- package/dist/backend/codex.d.ts +31 -0
- package/dist/backend/codex.js +100 -0
- package/dist/backend/codex.js.map +1 -0
- package/dist/backend/factory.d.ts +9 -0
- package/dist/backend/factory.js +56 -0
- package/dist/backend/factory.js.map +1 -0
- package/dist/backend/types.d.ts +54 -0
- package/dist/backend/types.js +2 -0
- package/dist/backend/types.js.map +1 -0
- package/dist/bridge/commands.d.ts +135 -0
- package/dist/bridge/commands.js +860 -0
- package/dist/bridge/commands.js.map +1 -0
- package/dist/bridge/service.d.ts +160 -0
- package/dist/bridge/service.js +3785 -0
- package/dist/bridge/service.js.map +1 -0
- package/dist/bridge/task-queue.d.ts +14 -0
- package/dist/bridge/task-queue.js +81 -0
- package/dist/bridge/task-queue.js.map +1 -0
- package/dist/bridge/types.d.ts +39 -0
- package/dist/bridge/types.js +2 -0
- package/dist/bridge/types.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1199 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex/capabilities.d.ts +20 -0
- package/dist/codex/capabilities.js +41 -0
- package/dist/codex/capabilities.js.map +1 -0
- package/dist/codex/runner.d.ts +47 -0
- package/dist/codex/runner.js +294 -0
- package/dist/codex/runner.js.map +1 -0
- package/dist/codex/session-index.d.ts +22 -0
- package/dist/codex/session-index.js +205 -0
- package/dist/codex/session-index.js.map +1 -0
- package/dist/collaboration/awareness.d.ts +36 -0
- package/dist/collaboration/awareness.js +107 -0
- package/dist/collaboration/awareness.js.map +1 -0
- package/dist/collaboration/digest.d.ts +65 -0
- package/dist/collaboration/digest.js +178 -0
- package/dist/collaboration/digest.js.map +1 -0
- package/dist/collaboration/handoff.d.ts +66 -0
- package/dist/collaboration/handoff.js +94 -0
- package/dist/collaboration/handoff.js.map +1 -0
- package/dist/collaboration/insights.d.ts +24 -0
- package/dist/collaboration/insights.js +243 -0
- package/dist/collaboration/insights.js.map +1 -0
- package/dist/collaboration/knowledge.d.ts +26 -0
- package/dist/collaboration/knowledge.js +105 -0
- package/dist/collaboration/knowledge.js.map +1 -0
- package/dist/collaboration/timeline.d.ts +31 -0
- package/dist/collaboration/timeline.js +150 -0
- package/dist/collaboration/timeline.js.map +1 -0
- package/dist/collaboration/trust.d.ts +49 -0
- package/dist/collaboration/trust.js +176 -0
- package/dist/collaboration/trust.js.map +1 -0
- package/dist/config/codex-skill.d.ts +7 -0
- package/dist/config/codex-skill.js +44 -0
- package/dist/config/codex-skill.js.map +1 -0
- package/dist/config/doctor.d.ts +12 -0
- package/dist/config/doctor.js +314 -0
- package/dist/config/doctor.js.map +1 -0
- package/dist/config/init.d.ts +3 -0
- package/dist/config/init.js +123 -0
- package/dist/config/init.js.map +1 -0
- package/dist/config/load.d.ts +33 -0
- package/dist/config/load.js +252 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/mutate.d.ts +21 -0
- package/dist/config/mutate.js +86 -0
- package/dist/config/mutate.js.map +1 -0
- package/dist/config/paths.d.ts +3 -0
- package/dist/config/paths.js +33 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/schema.d.ts +308 -0
- package/dist/config/schema.js +250 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/control-plane/project-session.d.ts +67 -0
- package/dist/control-plane/project-session.js +234 -0
- package/dist/control-plane/project-session.js.map +1 -0
- package/dist/feishu/base.d.ts +19 -0
- package/dist/feishu/base.js +93 -0
- package/dist/feishu/base.js.map +1 -0
- package/dist/feishu/cards.d.ts +22 -0
- package/dist/feishu/cards.js +144 -0
- package/dist/feishu/cards.js.map +1 -0
- package/dist/feishu/client.d.ts +61 -0
- package/dist/feishu/client.js +315 -0
- package/dist/feishu/client.js.map +1 -0
- package/dist/feishu/diagnostics.d.ts +42 -0
- package/dist/feishu/diagnostics.js +194 -0
- package/dist/feishu/diagnostics.js.map +1 -0
- package/dist/feishu/doc.d.ts +13 -0
- package/dist/feishu/doc.js +59 -0
- package/dist/feishu/doc.js.map +1 -0
- package/dist/feishu/extractors.d.ts +7 -0
- package/dist/feishu/extractors.js +215 -0
- package/dist/feishu/extractors.js.map +1 -0
- package/dist/feishu/long-connection.d.ts +12 -0
- package/dist/feishu/long-connection.js +41 -0
- package/dist/feishu/long-connection.js.map +1 -0
- package/dist/feishu/message-resource.d.ts +14 -0
- package/dist/feishu/message-resource.js +309 -0
- package/dist/feishu/message-resource.js.map +1 -0
- package/dist/feishu/replay.d.ts +37 -0
- package/dist/feishu/replay.js +114 -0
- package/dist/feishu/replay.js.map +1 -0
- package/dist/feishu/task.d.ts +18 -0
- package/dist/feishu/task.js +86 -0
- package/dist/feishu/task.js.map +1 -0
- package/dist/feishu/text.d.ts +23 -0
- package/dist/feishu/text.js +155 -0
- package/dist/feishu/text.js.map +1 -0
- package/dist/feishu/webhook.d.ts +23 -0
- package/dist/feishu/webhook.js +130 -0
- package/dist/feishu/webhook.js.map +1 -0
- package/dist/feishu/wiki.d.ts +52 -0
- package/dist/feishu/wiki.js +300 -0
- package/dist/feishu/wiki.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge/search.d.ts +11 -0
- package/dist/knowledge/search.js +83 -0
- package/dist/knowledge/search.js.map +1 -0
- package/dist/logging.d.ts +3 -0
- package/dist/logging.js +40 -0
- package/dist/logging.js.map +1 -0
- package/dist/mcp/server.d.ts +34 -0
- package/dist/mcp/server.js +1196 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/memory/embedding-factory.d.ts +6 -0
- package/dist/memory/embedding-factory.js +20 -0
- package/dist/memory/embedding-factory.js.map +1 -0
- package/dist/memory/embeddings.d.ts +40 -0
- package/dist/memory/embeddings.js +150 -0
- package/dist/memory/embeddings.js.map +1 -0
- package/dist/memory/ollama-embeddings.d.ts +63 -0
- package/dist/memory/ollama-embeddings.js +215 -0
- package/dist/memory/ollama-embeddings.js.map +1 -0
- package/dist/memory/retrieve.d.ts +17 -0
- package/dist/memory/retrieve.js +29 -0
- package/dist/memory/retrieve.js.map +1 -0
- package/dist/memory/summarize.d.ts +13 -0
- package/dist/memory/summarize.js +58 -0
- package/dist/memory/summarize.js.map +1 -0
- package/dist/observability/cost.d.ts +12 -0
- package/dist/observability/cost.js +22 -0
- package/dist/observability/cost.js.map +1 -0
- package/dist/observability/dashboard-html.d.ts +5 -0
- package/dist/observability/dashboard-html.js +304 -0
- package/dist/observability/dashboard-html.js.map +1 -0
- package/dist/observability/metrics.d.ts +36 -0
- package/dist/observability/metrics.js +230 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/readiness.d.ts +31 -0
- package/dist/observability/readiness.js +57 -0
- package/dist/observability/readiness.js.map +1 -0
- package/dist/observability/server.d.ts +84 -0
- package/dist/observability/server.js +181 -0
- package/dist/observability/server.js.map +1 -0
- package/dist/projects/paths.d.ts +9 -0
- package/dist/projects/paths.js +30 -0
- package/dist/projects/paths.js.map +1 -0
- package/dist/runtime/instance-lock.d.ts +12 -0
- package/dist/runtime/instance-lock.js +99 -0
- package/dist/runtime/instance-lock.js.map +1 -0
- package/dist/runtime/process.d.ts +2 -0
- package/dist/runtime/process.js +43 -0
- package/dist/runtime/process.js.map +1 -0
- package/dist/runtime/shutdown.d.ts +11 -0
- package/dist/runtime/shutdown.js +38 -0
- package/dist/runtime/shutdown.js.map +1 -0
- package/dist/security/access.d.ts +13 -0
- package/dist/security/access.js +160 -0
- package/dist/security/access.js.map +1 -0
- package/dist/service/install.d.ts +19 -0
- package/dist/service/install.js +35 -0
- package/dist/service/install.js.map +1 -0
- package/dist/service/templates.d.ts +22 -0
- package/dist/service/templates.js +118 -0
- package/dist/service/templates.js.map +1 -0
- package/dist/state/audit-log.d.ts +33 -0
- package/dist/state/audit-log.js +116 -0
- package/dist/state/audit-log.js.map +1 -0
- package/dist/state/config-history-store.d.ts +27 -0
- package/dist/state/config-history-store.js +65 -0
- package/dist/state/config-history-store.js.map +1 -0
- package/dist/state/handoff-store.d.ts +20 -0
- package/dist/state/handoff-store.js +97 -0
- package/dist/state/handoff-store.js.map +1 -0
- package/dist/state/idempotency-store.d.ts +19 -0
- package/dist/state/idempotency-store.js +84 -0
- package/dist/state/idempotency-store.js.map +1 -0
- package/dist/state/memory-store.d.ts +137 -0
- package/dist/state/memory-store.js +713 -0
- package/dist/state/memory-store.js.map +1 -0
- package/dist/state/pending-command-store.d.ts +30 -0
- package/dist/state/pending-command-store.js +108 -0
- package/dist/state/pending-command-store.js.map +1 -0
- package/dist/state/run-state-store.d.ts +58 -0
- package/dist/state/run-state-store.js +269 -0
- package/dist/state/run-state-store.js.map +1 -0
- package/dist/state/session-store.d.ts +56 -0
- package/dist/state/session-store.js +275 -0
- package/dist/state/session-store.js.map +1 -0
- package/dist/state/trust-store.d.ts +15 -0
- package/dist/state/trust-store.js +53 -0
- package/dist/state/trust-store.js.map +1 -0
- package/dist/utils/fs.d.ts +4 -0
- package/dist/utils/fs.js +26 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/json.d.ts +1 -0
- package/dist/utils/json.js +9 -0
- package/dist/utils/json.js.map +1 -0
- package/dist/utils/path.d.ts +3 -0
- package/dist/utils/path.js +22 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/serial-executor.d.ts +5 -0
- package/dist/utils/serial-executor.js +12 -0
- package/dist/utils/serial-executor.js.map +1 -0
- package/package.json +71 -0
- package/skills/feique-session/SKILL.md +27 -0
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import packageJson from '../../package.json' with { type: 'json' };
|
|
7
|
+
import { buildHelpText, describeBridgeCommand, parseBridgeCommand } from '../bridge/commands.js';
|
|
8
|
+
import { buildQueueKey } from '../bridge/service.js';
|
|
9
|
+
import { adoptProjectSession as adoptSharedProjectSession, listBridgeSessions as listSharedBridgeSessions, resolveProjectContext as resolveSharedProjectContext, switchProjectBinding as switchSharedProjectBinding } from '../control-plane/project-session.js';
|
|
10
|
+
import { CodexSessionIndex } from '../codex/session-index.js';
|
|
11
|
+
import { loadBridgeConfig, loadRuntimeConfig } from '../config/load.js';
|
|
12
|
+
import { createProjectAlias } from '../config/mutate.js';
|
|
13
|
+
import { canAccessGlobalCapability, canAccessProjectCapability, describeMinimumRole, filterAccessibleProjects } from '../security/access.js';
|
|
14
|
+
import { ConfigHistoryStore } from '../state/config-history-store.js';
|
|
15
|
+
import { RunStateStore } from '../state/run-state-store.js';
|
|
16
|
+
import { SessionStore, buildConversationKey } from '../state/session-store.js';
|
|
17
|
+
import { fileExists, writeUtf8Atomic } from '../utils/fs.js';
|
|
18
|
+
import { getProjectCacheDir, getProjectDownloadsDir, getProjectLogDir, getProjectTempDir } from '../projects/paths.js';
|
|
19
|
+
import { AuditLog } from '../state/audit-log.js';
|
|
20
|
+
import { MemoryStore } from '../state/memory-store.js';
|
|
21
|
+
import { TrustStore } from '../state/trust-store.js';
|
|
22
|
+
import { buildTeamActivityView, formatTeamView } from '../collaboration/awareness.js';
|
|
23
|
+
import { analyzeTeamHealth, formatInsightsReport } from '../collaboration/insights.js';
|
|
24
|
+
import { buildProjectTimeline, formatTimeline } from '../collaboration/timeline.js';
|
|
25
|
+
import { formatTrustState, createInitialTrustState } from '../collaboration/trust.js';
|
|
26
|
+
const CONVERSATION_SCHEMA_PROPERTIES = {
|
|
27
|
+
chatId: { type: 'string' },
|
|
28
|
+
actorId: { type: 'string' },
|
|
29
|
+
tenantKey: { type: 'string' },
|
|
30
|
+
projectAlias: { type: 'string' },
|
|
31
|
+
};
|
|
32
|
+
const TOOL_DEFINITIONS = [
|
|
33
|
+
{
|
|
34
|
+
name: 'projects.list',
|
|
35
|
+
description: 'List configured projects and their key isolation settings.',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
properties: {},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'project.switch',
|
|
44
|
+
description: 'Switch the bound project for one MCP conversation and optionally auto-adopt the latest local Codex session.',
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
properties: {
|
|
49
|
+
...CONVERSATION_SCHEMA_PROPERTIES,
|
|
50
|
+
},
|
|
51
|
+
required: ['chatId', 'projectAlias'],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'project.create',
|
|
56
|
+
description: 'Create a project directory on disk and bind it as a new project alias in the writable config.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
properties: {
|
|
61
|
+
...CONVERSATION_SCHEMA_PROPERTIES,
|
|
62
|
+
root: { type: 'string' },
|
|
63
|
+
},
|
|
64
|
+
required: ['chatId', 'projectAlias', 'root'],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'sessions.list',
|
|
69
|
+
description: 'List saved bridge sessions for one MCP conversation and project.',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
properties: {
|
|
74
|
+
...CONVERSATION_SCHEMA_PROPERTIES,
|
|
75
|
+
},
|
|
76
|
+
required: ['chatId'],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'session.adopt',
|
|
81
|
+
description: 'Adopt the latest or a specific local CLI session (Codex or Claude) for one MCP conversation. Use target=list to inspect candidates.',
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
properties: {
|
|
86
|
+
...CONVERSATION_SCHEMA_PROPERTIES,
|
|
87
|
+
target: { type: 'string' },
|
|
88
|
+
},
|
|
89
|
+
required: ['chatId'],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'status.get',
|
|
94
|
+
description: 'Return runtime status, pid, active runs, and key storage paths.',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
additionalProperties: false,
|
|
98
|
+
properties: {},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'runs.list',
|
|
103
|
+
description: 'List active runs or all saved runs from the local state store.',
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
additionalProperties: false,
|
|
107
|
+
properties: {
|
|
108
|
+
all: { type: 'boolean' },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'command.interpret',
|
|
114
|
+
description: 'Interpret slash commands or natural-language control intents without executing them.',
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
properties: {
|
|
119
|
+
text: { type: 'string' },
|
|
120
|
+
},
|
|
121
|
+
required: ['text'],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'command.execute',
|
|
126
|
+
description: 'Execute supported slash commands or natural-language control intents over MCP.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
properties: {
|
|
131
|
+
...CONVERSATION_SCHEMA_PROPERTIES,
|
|
132
|
+
text: { type: 'string' },
|
|
133
|
+
},
|
|
134
|
+
required: ['chatId', 'text'],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'config.history',
|
|
139
|
+
description: 'Return recent config snapshots recorded by admin operations.',
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
additionalProperties: false,
|
|
143
|
+
properties: {
|
|
144
|
+
chatId: { type: 'string' },
|
|
145
|
+
limit: { type: 'integer', minimum: 1, maximum: 20 },
|
|
146
|
+
},
|
|
147
|
+
required: ['chatId'],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'config.rollback',
|
|
152
|
+
description: 'Roll back the writable config file to a previous snapshot. A service restart may still be required.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
properties: {
|
|
157
|
+
chatId: { type: 'string' },
|
|
158
|
+
target: { type: 'string' },
|
|
159
|
+
},
|
|
160
|
+
required: ['chatId'],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'service.restart',
|
|
165
|
+
description: 'Restart the feique background service with the current config.',
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
additionalProperties: false,
|
|
169
|
+
properties: {
|
|
170
|
+
chatId: { type: 'string' },
|
|
171
|
+
},
|
|
172
|
+
required: ['chatId'],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'team.activity',
|
|
177
|
+
description: 'Get current team activity view showing who is working on what.',
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
additionalProperties: false,
|
|
181
|
+
properties: {},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'team.insights',
|
|
186
|
+
description: 'Get team health report: retry patterns, duplicate work, queue bottlenecks, error clusters.',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
additionalProperties: false,
|
|
190
|
+
properties: {},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'project.timeline',
|
|
195
|
+
description: 'Get project activity timeline including runs, knowledge, and config changes.',
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
additionalProperties: false,
|
|
199
|
+
properties: {
|
|
200
|
+
projectAlias: { type: 'string' },
|
|
201
|
+
},
|
|
202
|
+
required: ['projectAlias'],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'project.trust',
|
|
207
|
+
description: 'Get or set the trust level for a project. Trust levels control what AI can do autonomously.',
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
additionalProperties: false,
|
|
211
|
+
properties: {
|
|
212
|
+
projectAlias: { type: 'string' },
|
|
213
|
+
level: { type: 'string', enum: ['observe', 'suggest', 'execute', 'autonomous'] },
|
|
214
|
+
},
|
|
215
|
+
required: ['projectAlias'],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
export async function startMcpServer(options) {
|
|
220
|
+
const { config } = await loadBridgeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
221
|
+
const transport = options.transport ?? config.mcp.transport;
|
|
222
|
+
if (transport === 'http') {
|
|
223
|
+
await startHttpMcpServer(options, config);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const parser = new StdioMessageParser(async (request) => {
|
|
227
|
+
const response = await handleMcpRequest(request, options);
|
|
228
|
+
if (response) {
|
|
229
|
+
process.stdout.write(encodeMessage(response));
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
process.stdin.on('data', (chunk) => {
|
|
233
|
+
parser.push(chunk);
|
|
234
|
+
});
|
|
235
|
+
process.stdin.on('end', () => {
|
|
236
|
+
process.exit(0);
|
|
237
|
+
});
|
|
238
|
+
process.stdin.resume();
|
|
239
|
+
}
|
|
240
|
+
export async function handleMcpRequest(request, options) {
|
|
241
|
+
if (request.method === 'notifications/initialized') {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
if (request.method === 'initialize') {
|
|
245
|
+
return {
|
|
246
|
+
jsonrpc: '2.0',
|
|
247
|
+
id: request.id ?? null,
|
|
248
|
+
result: {
|
|
249
|
+
protocolVersion: typeof request.params?.protocolVersion === 'string' ? request.params.protocolVersion : '2025-03-26',
|
|
250
|
+
capabilities: {
|
|
251
|
+
tools: {},
|
|
252
|
+
},
|
|
253
|
+
serverInfo: {
|
|
254
|
+
name: 'feique',
|
|
255
|
+
version: packageJson.version,
|
|
256
|
+
},
|
|
257
|
+
instructions: 'Use the provided tools to inspect feique runtime state, switch projects, adopt Codex sessions, and safely interpret or execute supported control commands.',
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (request.method === 'ping') {
|
|
262
|
+
return {
|
|
263
|
+
jsonrpc: '2.0',
|
|
264
|
+
id: request.id ?? null,
|
|
265
|
+
result: {},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (request.method === 'tools/list') {
|
|
269
|
+
return {
|
|
270
|
+
jsonrpc: '2.0',
|
|
271
|
+
id: request.id ?? null,
|
|
272
|
+
result: {
|
|
273
|
+
tools: TOOL_DEFINITIONS,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (request.method === 'tools/call') {
|
|
278
|
+
try {
|
|
279
|
+
const result = await handleToolCall(request.params ?? {}, options);
|
|
280
|
+
return {
|
|
281
|
+
jsonrpc: '2.0',
|
|
282
|
+
id: request.id ?? null,
|
|
283
|
+
result,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
return {
|
|
288
|
+
jsonrpc: '2.0',
|
|
289
|
+
id: request.id ?? null,
|
|
290
|
+
result: {
|
|
291
|
+
isError: true,
|
|
292
|
+
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
jsonrpc: '2.0',
|
|
299
|
+
id: request.id ?? null,
|
|
300
|
+
error: {
|
|
301
|
+
code: -32601,
|
|
302
|
+
message: `Method not found: ${request.method}`,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
async function handleToolCall(params, options) {
|
|
307
|
+
const name = typeof params.name === 'string' ? params.name : '';
|
|
308
|
+
const argumentsObject = isPlainObject(params.arguments) ? params.arguments : {};
|
|
309
|
+
switch (name) {
|
|
310
|
+
case 'projects.list': {
|
|
311
|
+
const { config } = await loadBridgeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
312
|
+
const projects = Object.entries(config.projects).map(([alias, project]) => ({
|
|
313
|
+
alias,
|
|
314
|
+
root: project.root,
|
|
315
|
+
session_scope: project.session_scope,
|
|
316
|
+
mention_required: project.mention_required,
|
|
317
|
+
admin_chat_ids: project.admin_chat_ids,
|
|
318
|
+
session_operator_chat_ids: project.session_operator_chat_ids ?? [],
|
|
319
|
+
run_operator_chat_ids: project.run_operator_chat_ids ?? [],
|
|
320
|
+
config_admin_chat_ids: project.config_admin_chat_ids ?? [],
|
|
321
|
+
download_dir: getProjectDownloadsDir(config.storage.dir, alias, project),
|
|
322
|
+
temp_dir: getProjectTempDir(config.storage.dir, alias, project),
|
|
323
|
+
cache_dir: getProjectCacheDir(config.storage.dir, alias, project),
|
|
324
|
+
log_dir: getProjectLogDir(config.storage.dir, alias, project),
|
|
325
|
+
chat_rate_limit_window_seconds: project.chat_rate_limit_window_seconds,
|
|
326
|
+
chat_rate_limit_max_runs: project.chat_rate_limit_max_runs,
|
|
327
|
+
}));
|
|
328
|
+
return buildToolResult(projects.length > 0 ? renderJson(projects) : 'No projects configured.', { projects });
|
|
329
|
+
}
|
|
330
|
+
case 'project.switch': {
|
|
331
|
+
const { config } = await loadBridgeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
332
|
+
const sessionStore = new SessionStore(config.storage.dir);
|
|
333
|
+
const sessionIndex = new CodexSessionIndex();
|
|
334
|
+
const switched = await switchProjectBinding(config, sessionStore, sessionIndex, parseConversationInput(argumentsObject), requireString(argumentsObject, 'projectAlias'));
|
|
335
|
+
return buildToolResult(switched.text, switched.structured);
|
|
336
|
+
}
|
|
337
|
+
case 'project.create': {
|
|
338
|
+
const { config, sources } = await loadBridgeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
339
|
+
const chatId = requireString(argumentsObject, 'chatId');
|
|
340
|
+
if (!canAccessGlobalCapability(config, chatId, 'config:mutate')) {
|
|
341
|
+
throw new Error('Current chat is not allowed to create projects.');
|
|
342
|
+
}
|
|
343
|
+
const writableConfigPath = resolveWritableConfigPath(options.configPath, sources);
|
|
344
|
+
if (!writableConfigPath) {
|
|
345
|
+
throw new Error('No writable config path resolved for project creation.');
|
|
346
|
+
}
|
|
347
|
+
const alias = requireString(argumentsObject, 'projectAlias');
|
|
348
|
+
if (config.projects[alias]) {
|
|
349
|
+
throw new Error(`Project alias already exists: ${alias}`);
|
|
350
|
+
}
|
|
351
|
+
const root = requireString(argumentsObject, 'root');
|
|
352
|
+
const history = new ConfigHistoryStore(config.storage.dir);
|
|
353
|
+
const snapshot = await history.recordSnapshot({
|
|
354
|
+
configPath: writableConfigPath,
|
|
355
|
+
action: 'project.create',
|
|
356
|
+
summary: `${alias} -> ${root}`,
|
|
357
|
+
chatId,
|
|
358
|
+
actorId: readOptionalString(argumentsObject, 'actorId'),
|
|
359
|
+
limit: 5,
|
|
360
|
+
});
|
|
361
|
+
const created = await createProjectAlias({
|
|
362
|
+
configPath: writableConfigPath,
|
|
363
|
+
alias,
|
|
364
|
+
root,
|
|
365
|
+
});
|
|
366
|
+
return buildToolResult(`Created project ${alias} at ${created.root}. Restart the service if it should pick up the new project immediately.`, { alias, root: created.root, snapshotId: snapshot.id, created: true });
|
|
367
|
+
}
|
|
368
|
+
case 'sessions.list': {
|
|
369
|
+
const { config } = await loadBridgeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
370
|
+
const sessionStore = new SessionStore(config.storage.dir);
|
|
371
|
+
const listing = await listBridgeSessions(config, sessionStore, parseConversationInput(argumentsObject));
|
|
372
|
+
return buildToolResult(listing.text, listing.structured);
|
|
373
|
+
}
|
|
374
|
+
case 'session.adopt': {
|
|
375
|
+
const { config } = await loadBridgeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
376
|
+
const sessionStore = new SessionStore(config.storage.dir);
|
|
377
|
+
const sessionIndex = new CodexSessionIndex();
|
|
378
|
+
const adopted = await adoptProjectSession(config, sessionStore, sessionIndex, parseConversationInput(argumentsObject), readOptionalString(argumentsObject, 'target'));
|
|
379
|
+
return buildToolResult(adopted.text, adopted.structured);
|
|
380
|
+
}
|
|
381
|
+
case 'status.get': {
|
|
382
|
+
const { config } = await loadRuntimeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
383
|
+
const status = await inspectRuntimeStatus(config);
|
|
384
|
+
return buildToolResult(renderJson(status), status);
|
|
385
|
+
}
|
|
386
|
+
case 'runs.list': {
|
|
387
|
+
const { config } = await loadRuntimeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
388
|
+
const runStateStore = new RunStateStore(config.storage.dir);
|
|
389
|
+
const runs = argumentsObject.all === true ? await runStateStore.listRuns() : await runStateStore.listActiveRuns();
|
|
390
|
+
return buildToolResult(renderJson(runs), { runs });
|
|
391
|
+
}
|
|
392
|
+
case 'command.interpret': {
|
|
393
|
+
const interpretation = interpretBridgeCommand(requireString(argumentsObject, 'text'));
|
|
394
|
+
return buildToolResult(interpretation.text, interpretation.structured);
|
|
395
|
+
}
|
|
396
|
+
case 'command.execute': {
|
|
397
|
+
const { config, sources } = await loadBridgeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
398
|
+
const sessionStore = new SessionStore(config.storage.dir);
|
|
399
|
+
const sessionIndex = new CodexSessionIndex();
|
|
400
|
+
const runStateStore = new RunStateStore(config.storage.dir);
|
|
401
|
+
const execution = await executeMcpCommand({
|
|
402
|
+
config,
|
|
403
|
+
writableConfigPath: resolveWritableConfigPath(options.configPath, sources),
|
|
404
|
+
cwd: options.cwd,
|
|
405
|
+
sessionStore,
|
|
406
|
+
sessionIndex,
|
|
407
|
+
runStateStore,
|
|
408
|
+
conversation: parseConversationInput(argumentsObject),
|
|
409
|
+
text: requireString(argumentsObject, 'text'),
|
|
410
|
+
});
|
|
411
|
+
return buildToolResult(execution.text, execution.structured);
|
|
412
|
+
}
|
|
413
|
+
case 'config.history': {
|
|
414
|
+
const { config, sources } = await loadBridgeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
415
|
+
if (!canAccessGlobalCapability(config, requireString(argumentsObject, 'chatId'), 'config:history')) {
|
|
416
|
+
throw new Error('Current chat is not allowed to inspect config history.');
|
|
417
|
+
}
|
|
418
|
+
const writableConfigPath = resolveWritableConfigPath(options.configPath, sources);
|
|
419
|
+
const store = new ConfigHistoryStore(config.storage.dir);
|
|
420
|
+
const limit = typeof argumentsObject.limit === 'number' ? Math.max(1, Math.min(20, Math.trunc(argumentsObject.limit))) : 5;
|
|
421
|
+
const snapshots = await store.listSnapshots(limit);
|
|
422
|
+
return buildToolResult(renderJson({ writableConfigPath, snapshots }), { writableConfigPath, snapshots });
|
|
423
|
+
}
|
|
424
|
+
case 'config.rollback': {
|
|
425
|
+
const { config, sources } = await loadBridgeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
426
|
+
if (!canAccessGlobalCapability(config, requireString(argumentsObject, 'chatId'), 'config:rollback')) {
|
|
427
|
+
throw new Error('Current chat is not allowed to roll back config.');
|
|
428
|
+
}
|
|
429
|
+
const writableConfigPath = resolveWritableConfigPath(options.configPath, sources);
|
|
430
|
+
if (!writableConfigPath) {
|
|
431
|
+
throw new Error('No writable config path resolved for rollback.');
|
|
432
|
+
}
|
|
433
|
+
const store = new ConfigHistoryStore(config.storage.dir);
|
|
434
|
+
const target = await store.getSnapshot(typeof argumentsObject.target === 'string' ? argumentsObject.target : undefined);
|
|
435
|
+
if (!target) {
|
|
436
|
+
throw new Error('Target config snapshot not found.');
|
|
437
|
+
}
|
|
438
|
+
await writeUtf8Atomic(writableConfigPath, target.content);
|
|
439
|
+
return buildToolResult(`Rolled back config to snapshot ${target.id}. Restart the service if you need in-memory config to reload.`, { snapshot: target.id, configPath: writableConfigPath });
|
|
440
|
+
}
|
|
441
|
+
case 'service.restart': {
|
|
442
|
+
const { config, sources } = await loadBridgeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
443
|
+
if (!canAccessGlobalCapability(config, requireString(argumentsObject, 'chatId'), 'service:restart')) {
|
|
444
|
+
throw new Error('Current chat is not allowed to restart the service.');
|
|
445
|
+
}
|
|
446
|
+
const writableConfigPath = resolveWritableConfigPath(options.configPath, sources);
|
|
447
|
+
await restartServiceProcess(options.cwd, writableConfigPath);
|
|
448
|
+
return buildToolResult('Service restart command submitted.', { restarted: true, service: config.service.name });
|
|
449
|
+
}
|
|
450
|
+
case 'team.activity': {
|
|
451
|
+
const { config } = await loadRuntimeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
452
|
+
const runStateStore = new RunStateStore(config.storage.dir);
|
|
453
|
+
const runs = await runStateStore.listRuns();
|
|
454
|
+
const activities = buildTeamActivityView(runs);
|
|
455
|
+
const text = formatTeamView(activities);
|
|
456
|
+
return buildToolResult(text, { activities });
|
|
457
|
+
}
|
|
458
|
+
case 'team.insights': {
|
|
459
|
+
const { config } = await loadRuntimeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
460
|
+
const runStateStore = new RunStateStore(config.storage.dir);
|
|
461
|
+
const auditLog = new AuditLog(config.storage.dir);
|
|
462
|
+
const runs = await runStateStore.listRuns();
|
|
463
|
+
const auditEvents = await auditLog.tail(500);
|
|
464
|
+
const insights = analyzeTeamHealth(runs, auditEvents);
|
|
465
|
+
const text = formatInsightsReport(insights);
|
|
466
|
+
return buildToolResult(text, { insights });
|
|
467
|
+
}
|
|
468
|
+
case 'project.timeline': {
|
|
469
|
+
const { config } = await loadRuntimeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
470
|
+
const projectAlias = requireString(argumentsObject, 'projectAlias');
|
|
471
|
+
const runStateStore = new RunStateStore(config.storage.dir);
|
|
472
|
+
const memoryStore = new MemoryStore(config.storage.dir);
|
|
473
|
+
const auditLog = new AuditLog(config.storage.dir);
|
|
474
|
+
const runs = await runStateStore.listRuns();
|
|
475
|
+
const memories = await memoryStore.listRecentProjectMemories(projectAlias, 100);
|
|
476
|
+
const auditEvents = await auditLog.tail(500);
|
|
477
|
+
const timeline = buildProjectTimeline(runs, memories, auditEvents, projectAlias);
|
|
478
|
+
const text = formatTimeline(timeline);
|
|
479
|
+
return buildToolResult(text, { projectAlias, timeline });
|
|
480
|
+
}
|
|
481
|
+
case 'project.trust': {
|
|
482
|
+
const { config } = await loadRuntimeConfig({ cwd: options.cwd, configPath: options.configPath });
|
|
483
|
+
const projectAlias = requireString(argumentsObject, 'projectAlias');
|
|
484
|
+
const trustStore = new TrustStore(config.storage.dir);
|
|
485
|
+
const levelInput = readOptionalString(argumentsObject, 'level');
|
|
486
|
+
if (levelInput) {
|
|
487
|
+
const validLevels = ['observe', 'suggest', 'execute', 'autonomous'];
|
|
488
|
+
if (!validLevels.includes(levelInput)) {
|
|
489
|
+
throw new Error(`Invalid trust level: ${levelInput}. Must be one of: ${validLevels.join(', ')}`);
|
|
490
|
+
}
|
|
491
|
+
const existing = await trustStore.get(projectAlias);
|
|
492
|
+
const updated = existing
|
|
493
|
+
? { ...existing, current_level: levelInput, last_evaluated_at: new Date().toISOString() }
|
|
494
|
+
: createInitialTrustState(projectAlias, levelInput);
|
|
495
|
+
await trustStore.update(projectAlias, updated);
|
|
496
|
+
return buildToolResult(`已将项目 ${projectAlias} 的信任等级设置为: ${levelInput}`, { projectAlias, level: levelInput, state: updated });
|
|
497
|
+
}
|
|
498
|
+
const state = await trustStore.getOrCreate(projectAlias);
|
|
499
|
+
const text = formatTrustState(state);
|
|
500
|
+
return buildToolResult(text, { projectAlias, state });
|
|
501
|
+
}
|
|
502
|
+
default:
|
|
503
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async function startHttpMcpServer(options, config) {
|
|
507
|
+
const host = options.host ?? config.mcp.host;
|
|
508
|
+
const port = options.port ?? config.mcp.port;
|
|
509
|
+
const rpcPath = options.path ?? config.mcp.path;
|
|
510
|
+
const ssePath = options.ssePath ?? config.mcp.sse_path;
|
|
511
|
+
const messagePath = options.messagePath ?? config.mcp.message_path;
|
|
512
|
+
const auth = resolveHttpAuthTokens(config, options);
|
|
513
|
+
const sessions = new Map();
|
|
514
|
+
const server = http.createServer(async (request, response) => {
|
|
515
|
+
try {
|
|
516
|
+
const url = new URL(request.url ?? '/', `http://${request.headers.host ?? `${host}:${port}`}`);
|
|
517
|
+
if (!authorizeHttpRequest(request, auth)) {
|
|
518
|
+
response.statusCode = 401;
|
|
519
|
+
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
520
|
+
response.end(JSON.stringify({ error: 'Unauthorized MCP request.' }));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (request.method === 'GET' && url.pathname === ssePath) {
|
|
524
|
+
const sessionId = randomUUID();
|
|
525
|
+
response.writeHead(200, {
|
|
526
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
527
|
+
'cache-control': 'no-cache, no-transform',
|
|
528
|
+
connection: 'keep-alive',
|
|
529
|
+
});
|
|
530
|
+
response.write(`event: endpoint\ndata: ${JSON.stringify({ sessionId, rpcPath, messagePath: `${messagePath}?sessionId=${sessionId}` })}\n\n`);
|
|
531
|
+
const keepAlive = setInterval(() => {
|
|
532
|
+
response.write(`: keep-alive ${Date.now()}\n\n`);
|
|
533
|
+
}, 15000);
|
|
534
|
+
keepAlive.unref?.();
|
|
535
|
+
sessions.set(sessionId, { id: sessionId, response, keepAlive });
|
|
536
|
+
request.on('close', () => {
|
|
537
|
+
const session = sessions.get(sessionId);
|
|
538
|
+
if (session) {
|
|
539
|
+
clearInterval(session.keepAlive);
|
|
540
|
+
sessions.delete(sessionId);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (request.method === 'POST' && url.pathname === messagePath) {
|
|
546
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
547
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
548
|
+
response.statusCode = 404;
|
|
549
|
+
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
550
|
+
response.end(JSON.stringify({ error: 'Unknown MCP SSE session.' }));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const body = await readRequestBody(request);
|
|
554
|
+
const payload = JSON.parse(body);
|
|
555
|
+
const rpcResponse = await handleMcpRequest(payload, options);
|
|
556
|
+
if (rpcResponse) {
|
|
557
|
+
sessions.get(sessionId)?.response.write(`event: message\ndata: ${JSON.stringify(rpcResponse)}\n\n`);
|
|
558
|
+
}
|
|
559
|
+
response.statusCode = 202;
|
|
560
|
+
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
561
|
+
response.end(JSON.stringify({ accepted: true, sessionId }));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (request.method === 'POST' && url.pathname === rpcPath) {
|
|
565
|
+
const body = await readRequestBody(request);
|
|
566
|
+
const payload = JSON.parse(body);
|
|
567
|
+
const rpcResponse = await handleMcpRequest(payload, options);
|
|
568
|
+
response.statusCode = rpcResponse ? 200 : 204;
|
|
569
|
+
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
570
|
+
response.end(rpcResponse ? JSON.stringify(rpcResponse) : '');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (request.method === 'GET' && url.pathname === rpcPath) {
|
|
574
|
+
response.statusCode = 200;
|
|
575
|
+
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
576
|
+
response.end(JSON.stringify({
|
|
577
|
+
transport: 'http',
|
|
578
|
+
rpcPath,
|
|
579
|
+
ssePath,
|
|
580
|
+
messagePath,
|
|
581
|
+
auth: auth.length > 0 ? 'bearer' : 'none',
|
|
582
|
+
tokenIds: auth.map((token) => token.id),
|
|
583
|
+
activeTokenId: auth.find((token) => token.active)?.id ?? null,
|
|
584
|
+
}));
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
response.statusCode = 404;
|
|
588
|
+
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
589
|
+
response.end(JSON.stringify({ error: 'Not found.' }));
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
response.statusCode = 500;
|
|
593
|
+
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
594
|
+
response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
await new Promise((resolve, reject) => {
|
|
598
|
+
server.once('error', reject);
|
|
599
|
+
server.listen(port, host, () => {
|
|
600
|
+
resolve();
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
process.stderr.write(`MCP HTTP server listening on http://${host}:${port}${rpcPath}\n`);
|
|
604
|
+
await new Promise((resolve, reject) => {
|
|
605
|
+
const shutdown = () => {
|
|
606
|
+
for (const session of sessions.values()) {
|
|
607
|
+
clearInterval(session.keepAlive);
|
|
608
|
+
session.response.end();
|
|
609
|
+
}
|
|
610
|
+
sessions.clear();
|
|
611
|
+
server.close((error) => {
|
|
612
|
+
if (error) {
|
|
613
|
+
reject(error);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
resolve();
|
|
617
|
+
});
|
|
618
|
+
};
|
|
619
|
+
process.once('SIGINT', shutdown);
|
|
620
|
+
process.once('SIGTERM', shutdown);
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
async function switchProjectBinding(config, sessionStore, sessionIndex, conversation, projectAlias) {
|
|
624
|
+
return switchSharedProjectBinding(config, sessionStore, sessionIndex, conversation, projectAlias);
|
|
625
|
+
}
|
|
626
|
+
async function listBridgeSessions(config, sessionStore, conversation) {
|
|
627
|
+
return listSharedBridgeSessions(config, sessionStore, conversation);
|
|
628
|
+
}
|
|
629
|
+
async function adoptProjectSession(config, sessionStore, sessionIndex, conversation, target) {
|
|
630
|
+
return adoptSharedProjectSession(config, sessionStore, sessionIndex, conversation, target);
|
|
631
|
+
}
|
|
632
|
+
function interpretBridgeCommand(text) {
|
|
633
|
+
const command = parseBridgeCommand(text);
|
|
634
|
+
if (command.kind === 'prompt') {
|
|
635
|
+
return {
|
|
636
|
+
text: `普通提示词,不会作为 MCP 控制命令执行: ${truncateText(command.prompt, 120)}`,
|
|
637
|
+
structured: {
|
|
638
|
+
type: 'prompt',
|
|
639
|
+
prompt: command.prompt,
|
|
640
|
+
supported: false,
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
const supported = isMcpSupportedCommand(command);
|
|
645
|
+
return {
|
|
646
|
+
text: [
|
|
647
|
+
`命令摘要: ${describeBridgeCommand(command)}`,
|
|
648
|
+
`MCP 支持: ${supported ? 'yes' : 'no'}`,
|
|
649
|
+
].join('\n'),
|
|
650
|
+
structured: {
|
|
651
|
+
type: 'command',
|
|
652
|
+
command,
|
|
653
|
+
summary: describeBridgeCommand(command),
|
|
654
|
+
supported,
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
async function executeMcpCommand(input) {
|
|
659
|
+
const command = parseBridgeCommand(input.text);
|
|
660
|
+
if (command.kind === 'prompt') {
|
|
661
|
+
return {
|
|
662
|
+
text: `普通提示词,不会作为 MCP 控制命令执行: ${truncateText(command.prompt, 120)}`,
|
|
663
|
+
structured: {
|
|
664
|
+
executed: false,
|
|
665
|
+
type: 'prompt',
|
|
666
|
+
prompt: command.prompt,
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
if (!isMcpSupportedCommand(command)) {
|
|
671
|
+
return {
|
|
672
|
+
text: `当前 MCP 只支持部分控制命令,暂不支持: ${describeBridgeCommand(command)}`,
|
|
673
|
+
structured: {
|
|
674
|
+
executed: false,
|
|
675
|
+
summary: describeBridgeCommand(command),
|
|
676
|
+
supported: false,
|
|
677
|
+
command,
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
switch (command.kind) {
|
|
682
|
+
case 'help':
|
|
683
|
+
return {
|
|
684
|
+
text: buildHelpText(),
|
|
685
|
+
structured: {
|
|
686
|
+
executed: true,
|
|
687
|
+
kind: 'help',
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
case 'projects': {
|
|
691
|
+
const visibleAliases = filterAccessibleProjects(input.config, input.conversation.chatId);
|
|
692
|
+
const projects = Object.entries(input.config.projects)
|
|
693
|
+
.filter(([alias]) => visibleAliases.includes(alias))
|
|
694
|
+
.map(([alias, project]) => ({
|
|
695
|
+
alias,
|
|
696
|
+
root: project.root,
|
|
697
|
+
description: project.description ?? null,
|
|
698
|
+
session_scope: project.session_scope,
|
|
699
|
+
}));
|
|
700
|
+
const selected = await resolveSelectedProjectAlias(input.config, input.sessionStore, input.conversation);
|
|
701
|
+
return {
|
|
702
|
+
text: projects.length === 0 ? 'No accessible projects configured for this chat.' : renderJson({ selected, projects }),
|
|
703
|
+
structured: {
|
|
704
|
+
executed: true,
|
|
705
|
+
kind: 'projects',
|
|
706
|
+
selected,
|
|
707
|
+
projects,
|
|
708
|
+
},
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
case 'status': {
|
|
712
|
+
const status = await buildConversationStatus(input.config, input.sessionStore, input.runStateStore, input.conversation, command.detail === true);
|
|
713
|
+
return {
|
|
714
|
+
text: status.text,
|
|
715
|
+
structured: {
|
|
716
|
+
executed: true,
|
|
717
|
+
kind: 'status',
|
|
718
|
+
...status.structured,
|
|
719
|
+
},
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
case 'project':
|
|
723
|
+
if (!command.alias) {
|
|
724
|
+
const project = await resolveProjectContext(input.config, input.sessionStore, input.conversation);
|
|
725
|
+
return {
|
|
726
|
+
text: `当前项目: ${project.projectAlias}${project.project.description ? `\n说明: ${project.project.description}` : ''}`,
|
|
727
|
+
structured: {
|
|
728
|
+
executed: true,
|
|
729
|
+
kind: 'project',
|
|
730
|
+
projectAlias: project.projectAlias,
|
|
731
|
+
description: project.project.description ?? null,
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
const switched = await switchProjectBinding(input.config, input.sessionStore, input.sessionIndex, input.conversation, command.alias);
|
|
736
|
+
return {
|
|
737
|
+
text: switched.text,
|
|
738
|
+
structured: {
|
|
739
|
+
executed: true,
|
|
740
|
+
kind: 'project',
|
|
741
|
+
...switched.structured,
|
|
742
|
+
},
|
|
743
|
+
};
|
|
744
|
+
case 'session':
|
|
745
|
+
return executeSessionCommand(input.config, input.sessionStore, input.sessionIndex, input.conversation, command);
|
|
746
|
+
case 'admin':
|
|
747
|
+
return executeAdminServiceCommand(command, input);
|
|
748
|
+
default:
|
|
749
|
+
return {
|
|
750
|
+
text: `当前 MCP 暂不支持: ${describeBridgeCommand(command)}`,
|
|
751
|
+
structured: {
|
|
752
|
+
executed: false,
|
|
753
|
+
summary: describeBridgeCommand(command),
|
|
754
|
+
supported: false,
|
|
755
|
+
command,
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async function executeSessionCommand(config, sessionStore, sessionIndex, conversation, command) {
|
|
761
|
+
if (command.action === 'adopt') {
|
|
762
|
+
const adopted = await adoptProjectSession(config, sessionStore, sessionIndex, conversation, command.target);
|
|
763
|
+
return {
|
|
764
|
+
text: adopted.text,
|
|
765
|
+
structured: {
|
|
766
|
+
executed: true,
|
|
767
|
+
kind: 'session',
|
|
768
|
+
action: 'adopt',
|
|
769
|
+
...adopted.structured,
|
|
770
|
+
},
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
const resolved = await resolveProjectContext(config, sessionStore, conversation);
|
|
774
|
+
const sessions = await sessionStore.listProjectSessions(resolved.sessionKey, resolved.projectAlias);
|
|
775
|
+
const activeSessionId = (await sessionStore.getConversation(resolved.sessionKey))?.projects[resolved.projectAlias]?.thread_id ?? null;
|
|
776
|
+
switch (command.action) {
|
|
777
|
+
case 'list': {
|
|
778
|
+
const listing = await listBridgeSessions(config, sessionStore, conversation);
|
|
779
|
+
return {
|
|
780
|
+
text: listing.text,
|
|
781
|
+
structured: {
|
|
782
|
+
executed: true,
|
|
783
|
+
kind: 'session',
|
|
784
|
+
action: 'list',
|
|
785
|
+
...listing.structured,
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
case 'use': {
|
|
790
|
+
if (!canAccessProjectCapability(config, resolved.projectAlias, conversation.chatId, 'session:control')) {
|
|
791
|
+
throw new Error(`Current chat requires ${describeMinimumRole('operator')} role to switch sessions for ${resolved.projectAlias}.`);
|
|
792
|
+
}
|
|
793
|
+
if (!command.threadId) {
|
|
794
|
+
throw new Error('session use requires threadId.');
|
|
795
|
+
}
|
|
796
|
+
await sessionStore.setActiveProjectSession(resolved.sessionKey, resolved.projectAlias, command.threadId);
|
|
797
|
+
return {
|
|
798
|
+
text: `已切换到会话: ${command.threadId}`,
|
|
799
|
+
structured: {
|
|
800
|
+
executed: true,
|
|
801
|
+
kind: 'session',
|
|
802
|
+
action: 'use',
|
|
803
|
+
projectAlias: resolved.projectAlias,
|
|
804
|
+
sessionKey: resolved.sessionKey,
|
|
805
|
+
threadId: command.threadId,
|
|
806
|
+
},
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
case 'new':
|
|
810
|
+
if (!canAccessProjectCapability(config, resolved.projectAlias, conversation.chatId, 'session:control')) {
|
|
811
|
+
throw new Error(`Current chat requires ${describeMinimumRole('operator')} role to open a new session for ${resolved.projectAlias}.`);
|
|
812
|
+
}
|
|
813
|
+
await sessionStore.clearActiveProjectSession(resolved.sessionKey, resolved.projectAlias);
|
|
814
|
+
return {
|
|
815
|
+
text: '已切换为新会话模式。下一条消息会新开会话。',
|
|
816
|
+
structured: {
|
|
817
|
+
executed: true,
|
|
818
|
+
kind: 'session',
|
|
819
|
+
action: 'new',
|
|
820
|
+
projectAlias: resolved.projectAlias,
|
|
821
|
+
sessionKey: resolved.sessionKey,
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
case 'drop': {
|
|
825
|
+
if (!canAccessProjectCapability(config, resolved.projectAlias, conversation.chatId, 'session:control')) {
|
|
826
|
+
throw new Error(`Current chat requires ${describeMinimumRole('operator')} role to drop sessions for ${resolved.projectAlias}.`);
|
|
827
|
+
}
|
|
828
|
+
const targetThreadId = command.threadId ?? activeSessionId;
|
|
829
|
+
if (!targetThreadId) {
|
|
830
|
+
return {
|
|
831
|
+
text: '没有可删除的会话。',
|
|
832
|
+
structured: {
|
|
833
|
+
executed: true,
|
|
834
|
+
kind: 'session',
|
|
835
|
+
action: 'drop',
|
|
836
|
+
projectAlias: resolved.projectAlias,
|
|
837
|
+
sessionKey: resolved.sessionKey,
|
|
838
|
+
deleted: null,
|
|
839
|
+
},
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
await sessionStore.dropProjectSession(resolved.sessionKey, resolved.projectAlias, targetThreadId);
|
|
843
|
+
return {
|
|
844
|
+
text: `已删除会话: ${targetThreadId}`,
|
|
845
|
+
structured: {
|
|
846
|
+
executed: true,
|
|
847
|
+
kind: 'session',
|
|
848
|
+
action: 'drop',
|
|
849
|
+
projectAlias: resolved.projectAlias,
|
|
850
|
+
sessionKey: resolved.sessionKey,
|
|
851
|
+
deleted: targetThreadId,
|
|
852
|
+
remainingSessions: sessions.filter((session) => session.thread_id !== targetThreadId).length,
|
|
853
|
+
},
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
async function executeAdminServiceCommand(command, input) {
|
|
859
|
+
if (command.resource !== 'service') {
|
|
860
|
+
return {
|
|
861
|
+
text: `当前 MCP 暂不支持管理员命令: ${describeBridgeCommand(command)}`,
|
|
862
|
+
structured: {
|
|
863
|
+
executed: false,
|
|
864
|
+
summary: describeBridgeCommand(command),
|
|
865
|
+
supported: false,
|
|
866
|
+
},
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
if (command.action === 'status') {
|
|
870
|
+
const allowedAliases = filterAccessibleProjects(input.config, input.conversation.chatId, 'operator');
|
|
871
|
+
if (!canAccessGlobalCapability(input.config, input.conversation.chatId, 'service:status') && allowedAliases.length === 0) {
|
|
872
|
+
throw new Error(`Current chat requires ${describeMinimumRole('operator')} role to inspect service state.`);
|
|
873
|
+
}
|
|
874
|
+
const status = await inspectRuntimeStatus(input.config);
|
|
875
|
+
return {
|
|
876
|
+
text: renderJson(status),
|
|
877
|
+
structured: {
|
|
878
|
+
executed: true,
|
|
879
|
+
kind: 'admin',
|
|
880
|
+
resource: 'service',
|
|
881
|
+
action: 'status',
|
|
882
|
+
...status,
|
|
883
|
+
},
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
if (command.action === 'runs') {
|
|
887
|
+
const allowedAliases = new Set(filterAccessibleProjects(input.config, input.conversation.chatId, 'operator'));
|
|
888
|
+
if (!canAccessGlobalCapability(input.config, input.conversation.chatId, 'service:runs') && allowedAliases.size === 0) {
|
|
889
|
+
throw new Error(`Current chat requires ${describeMinimumRole('operator')} role to inspect active runs.`);
|
|
890
|
+
}
|
|
891
|
+
const runs = await input.runStateStore.listRuns();
|
|
892
|
+
const visibleRuns = canAccessGlobalCapability(input.config, input.conversation.chatId, 'service:runs')
|
|
893
|
+
? runs
|
|
894
|
+
: runs.filter((run) => allowedAliases.has(run.project_alias));
|
|
895
|
+
const active = visibleRuns.filter((run) => run.status === 'queued' || run.status === 'running' || run.status === 'orphaned').slice(0, 10);
|
|
896
|
+
const recentFailures = visibleRuns.filter((run) => run.status === 'failure' || run.status === 'cancelled' || run.status === 'stale').slice(0, 5);
|
|
897
|
+
const lines = ['当前运行列表', '', 'active/queued:'];
|
|
898
|
+
if (active.length === 0) {
|
|
899
|
+
lines.push('(empty)');
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
for (const run of active) {
|
|
903
|
+
lines.push(`- ${run.project_alias} | ${run.status} | chat=${run.chat_id} | ${run.updated_at}`);
|
|
904
|
+
if (run.status_detail) {
|
|
905
|
+
lines.push(` detail=${truncateText(run.status_detail, 120)}`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (recentFailures.length > 0) {
|
|
910
|
+
lines.push('', '最近失败:');
|
|
911
|
+
for (const run of recentFailures) {
|
|
912
|
+
lines.push(`- ${run.project_alias} | ${run.status} | ${run.updated_at}`);
|
|
913
|
+
lines.push(` error=${truncateText(run.error ?? 'unknown', 120)}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
text: lines.join('\n'),
|
|
918
|
+
structured: {
|
|
919
|
+
executed: true,
|
|
920
|
+
kind: 'admin',
|
|
921
|
+
resource: 'service',
|
|
922
|
+
action: 'runs',
|
|
923
|
+
active,
|
|
924
|
+
recentFailures,
|
|
925
|
+
},
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
const adminAliases = filterAccessibleProjects(input.config, input.conversation.chatId, 'admin');
|
|
929
|
+
if (!canAccessGlobalCapability(input.config, input.conversation.chatId, 'service:restart') && adminAliases.length === 0) {
|
|
930
|
+
throw new Error(`Current chat requires ${describeMinimumRole('admin')} role to restart the service.`);
|
|
931
|
+
}
|
|
932
|
+
await restartServiceProcess(input.cwd, input.writableConfigPath);
|
|
933
|
+
return {
|
|
934
|
+
text: 'Service restart command submitted.',
|
|
935
|
+
structured: {
|
|
936
|
+
executed: true,
|
|
937
|
+
kind: 'admin',
|
|
938
|
+
resource: 'service',
|
|
939
|
+
action: 'restart',
|
|
940
|
+
restarted: true,
|
|
941
|
+
service: input.config.service.name,
|
|
942
|
+
},
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
async function buildConversationStatus(config, sessionStore, runStateStore, conversation, detail) {
|
|
946
|
+
const resolved = await resolveProjectContext(config, sessionStore, conversation);
|
|
947
|
+
const conversationState = await sessionStore.getConversation(resolved.sessionKey);
|
|
948
|
+
const activeSessionId = conversationState?.projects[resolved.projectAlias]?.thread_id ?? null;
|
|
949
|
+
const sessions = await sessionStore.listProjectSessions(resolved.sessionKey, resolved.projectAlias);
|
|
950
|
+
const queueKey = buildQueueKey(resolved.sessionKey, resolved.projectAlias);
|
|
951
|
+
const activeRun = await runStateStore.getLatestVisibleRun(queueKey);
|
|
952
|
+
const runtime = await inspectRuntimeStatus(config);
|
|
953
|
+
const allRuns = detail ? await runStateStore.listRuns() : [];
|
|
954
|
+
const recentFailures = detail
|
|
955
|
+
? allRuns.filter((run) => run.queue_key === queueKey && (run.status === 'failure' || run.status === 'cancelled' || run.status === 'stale')).slice(0, 3)
|
|
956
|
+
: [];
|
|
957
|
+
const lines = [
|
|
958
|
+
`项目: ${resolved.projectAlias}`,
|
|
959
|
+
`项目根: ${resolved.project.root}`,
|
|
960
|
+
`会话键: ${resolved.sessionKey}`,
|
|
961
|
+
`当前会话: ${activeSessionId ?? '未选择'}`,
|
|
962
|
+
`保存会话数: ${sessions.length}`,
|
|
963
|
+
`服务运行: ${runtime.running ? 'yes' : 'no'}`,
|
|
964
|
+
`可见运行: ${activeRun ? activeRun.status : 'none'}`,
|
|
965
|
+
];
|
|
966
|
+
if (detail) {
|
|
967
|
+
if (activeRun?.status_detail) {
|
|
968
|
+
lines.push(`运行详情: ${activeRun.status_detail}`);
|
|
969
|
+
}
|
|
970
|
+
if (recentFailures.length > 0) {
|
|
971
|
+
lines.push('', '最近失败:');
|
|
972
|
+
for (const run of recentFailures) {
|
|
973
|
+
lines.push(`- ${run.status} | ${run.updated_at}`);
|
|
974
|
+
lines.push(` error=${truncateText(run.error ?? 'unknown', 120)}`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return {
|
|
979
|
+
text: lines.join('\n'),
|
|
980
|
+
structured: {
|
|
981
|
+
projectAlias: resolved.projectAlias,
|
|
982
|
+
projectRoot: resolved.project.root,
|
|
983
|
+
selectionKey: resolved.selectionKey,
|
|
984
|
+
sessionKey: resolved.sessionKey,
|
|
985
|
+
activeSessionId,
|
|
986
|
+
savedSessions: sessions.length,
|
|
987
|
+
activeRun,
|
|
988
|
+
runtime,
|
|
989
|
+
recentFailures,
|
|
990
|
+
},
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function isMcpSupportedCommand(command) {
|
|
994
|
+
switch (command.kind) {
|
|
995
|
+
case 'help':
|
|
996
|
+
case 'projects':
|
|
997
|
+
case 'status':
|
|
998
|
+
case 'project':
|
|
999
|
+
case 'session':
|
|
1000
|
+
return true;
|
|
1001
|
+
case 'admin':
|
|
1002
|
+
return command.resource === 'service';
|
|
1003
|
+
default:
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
async function resolveSelectedProjectAlias(config, sessionStore, conversation) {
|
|
1008
|
+
const selectionKey = buildConversationKey({
|
|
1009
|
+
tenantKey: conversation.tenantKey,
|
|
1010
|
+
chatId: conversation.chatId,
|
|
1011
|
+
actorId: conversation.actorId,
|
|
1012
|
+
scope: 'chat',
|
|
1013
|
+
});
|
|
1014
|
+
const selection = await sessionStore.getConversation(selectionKey);
|
|
1015
|
+
return conversation.projectAlias ?? selection?.selected_project_alias ?? config.service.default_project ?? Object.keys(config.projects)[0] ?? 'default';
|
|
1016
|
+
}
|
|
1017
|
+
async function resolveProjectContext(config, sessionStore, conversation) {
|
|
1018
|
+
return resolveSharedProjectContext(config, sessionStore, conversation);
|
|
1019
|
+
}
|
|
1020
|
+
function parseConversationInput(argumentsObject) {
|
|
1021
|
+
return {
|
|
1022
|
+
chatId: requireString(argumentsObject, 'chatId'),
|
|
1023
|
+
actorId: readOptionalString(argumentsObject, 'actorId'),
|
|
1024
|
+
tenantKey: readOptionalString(argumentsObject, 'tenantKey'),
|
|
1025
|
+
projectAlias: readOptionalString(argumentsObject, 'projectAlias'),
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function requireString(argumentsObject, key) {
|
|
1029
|
+
const value = readOptionalString(argumentsObject, key);
|
|
1030
|
+
if (!value) {
|
|
1031
|
+
throw new Error(`${key} is required.`);
|
|
1032
|
+
}
|
|
1033
|
+
return value;
|
|
1034
|
+
}
|
|
1035
|
+
function readOptionalString(argumentsObject, key) {
|
|
1036
|
+
return typeof argumentsObject[key] === 'string' && argumentsObject[key].trim().length > 0
|
|
1037
|
+
? String(argumentsObject[key]).trim()
|
|
1038
|
+
: undefined;
|
|
1039
|
+
}
|
|
1040
|
+
function buildToolResult(text, structuredContent) {
|
|
1041
|
+
return {
|
|
1042
|
+
content: [{ type: 'text', text }],
|
|
1043
|
+
...(structuredContent !== undefined ? { structuredContent } : {}),
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
function authorizeHttpRequest(request, tokens) {
|
|
1047
|
+
if (tokens.length === 0) {
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
const authorization = request.headers.authorization;
|
|
1051
|
+
return typeof authorization === 'string' && tokens.some((token) => authorization === `Bearer ${token.token}`);
|
|
1052
|
+
}
|
|
1053
|
+
function resolveHttpAuthTokens(config, options) {
|
|
1054
|
+
const now = Date.now();
|
|
1055
|
+
const resolved = [];
|
|
1056
|
+
const configured = options.authToken
|
|
1057
|
+
? [{ id: options.authTokenId ?? 'cli', token: options.authToken, enabled: true }]
|
|
1058
|
+
: config.mcp.auth_tokens;
|
|
1059
|
+
const activeId = options.authToken ? options.authTokenId ?? 'cli' : config.mcp.active_auth_token_id;
|
|
1060
|
+
for (const token of configured) {
|
|
1061
|
+
if (!token.token || token.enabled === false) {
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
if (token.expires_at) {
|
|
1065
|
+
const expiresAt = Date.parse(token.expires_at);
|
|
1066
|
+
if (!Number.isNaN(expiresAt) && expiresAt <= now) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
resolved.push({
|
|
1071
|
+
id: token.id,
|
|
1072
|
+
token: token.token,
|
|
1073
|
+
active: token.id === activeId,
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
if (!options.authToken && config.mcp.auth_token) {
|
|
1077
|
+
resolved.push({
|
|
1078
|
+
id: 'legacy',
|
|
1079
|
+
token: config.mcp.auth_token,
|
|
1080
|
+
active: !activeId,
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
if (resolved.length > 0 && !resolved.some((token) => token.active)) {
|
|
1084
|
+
resolved[0].active = true;
|
|
1085
|
+
}
|
|
1086
|
+
return resolved;
|
|
1087
|
+
}
|
|
1088
|
+
async function readRequestBody(request) {
|
|
1089
|
+
const chunks = [];
|
|
1090
|
+
for await (const chunk of request) {
|
|
1091
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1092
|
+
}
|
|
1093
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
1094
|
+
}
|
|
1095
|
+
class StdioMessageParser {
|
|
1096
|
+
onMessage;
|
|
1097
|
+
buffer = Buffer.alloc(0);
|
|
1098
|
+
constructor(onMessage) {
|
|
1099
|
+
this.onMessage = onMessage;
|
|
1100
|
+
}
|
|
1101
|
+
push(chunk) {
|
|
1102
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
1103
|
+
void this.drain();
|
|
1104
|
+
}
|
|
1105
|
+
async drain() {
|
|
1106
|
+
while (true) {
|
|
1107
|
+
const headerEnd = this.buffer.indexOf('\r\n\r\n');
|
|
1108
|
+
if (headerEnd < 0) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const headerText = this.buffer.slice(0, headerEnd).toString('utf8');
|
|
1112
|
+
const match = headerText.match(/Content-Length:\s*(\d+)/i);
|
|
1113
|
+
if (!match) {
|
|
1114
|
+
throw new Error('Missing Content-Length header.');
|
|
1115
|
+
}
|
|
1116
|
+
const contentLength = Number(match[1]);
|
|
1117
|
+
const messageStart = headerEnd + 4;
|
|
1118
|
+
const messageEnd = messageStart + contentLength;
|
|
1119
|
+
if (this.buffer.length < messageEnd) {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const payload = this.buffer.slice(messageStart, messageEnd).toString('utf8');
|
|
1123
|
+
this.buffer = this.buffer.slice(messageEnd);
|
|
1124
|
+
await this.onMessage(JSON.parse(payload));
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function encodeMessage(payload) {
|
|
1129
|
+
const body = JSON.stringify(payload);
|
|
1130
|
+
return `Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`;
|
|
1131
|
+
}
|
|
1132
|
+
async function inspectRuntimeStatus(config) {
|
|
1133
|
+
const pidPath = path.join(config.storage.dir, `${config.service.name}.pid`);
|
|
1134
|
+
const logPath = path.join(config.storage.dir, `${config.service.name}.log`);
|
|
1135
|
+
const pid = await readPid(pidPath);
|
|
1136
|
+
const runStateStore = new RunStateStore(config.storage.dir);
|
|
1137
|
+
return {
|
|
1138
|
+
running: pid !== null && (await isRunningPid(pid)),
|
|
1139
|
+
...(pid !== null ? { pid } : {}),
|
|
1140
|
+
pidPath,
|
|
1141
|
+
logPath,
|
|
1142
|
+
activeRuns: (await runStateStore.listActiveRuns()).length,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
async function readPid(filePath) {
|
|
1146
|
+
if (!(await fileExists(filePath))) {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
const raw = (await fs.readFile(filePath, 'utf8')).trim();
|
|
1150
|
+
const pid = Number(raw);
|
|
1151
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
1152
|
+
}
|
|
1153
|
+
async function isRunningPid(pid) {
|
|
1154
|
+
try {
|
|
1155
|
+
process.kill(pid, 0);
|
|
1156
|
+
return true;
|
|
1157
|
+
}
|
|
1158
|
+
catch {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
function resolveWritableConfigPath(explicitConfigPath, sources) {
|
|
1163
|
+
if (explicitConfigPath) {
|
|
1164
|
+
return path.resolve(explicitConfigPath);
|
|
1165
|
+
}
|
|
1166
|
+
return sources[0] ?? null;
|
|
1167
|
+
}
|
|
1168
|
+
async function restartServiceProcess(cwd, configPath) {
|
|
1169
|
+
const cliEntry = process.argv[1];
|
|
1170
|
+
if (!cliEntry) {
|
|
1171
|
+
throw new Error('Unable to resolve CLI entry for restart.');
|
|
1172
|
+
}
|
|
1173
|
+
await new Promise((resolve, reject) => {
|
|
1174
|
+
const args = [...process.execArgv, cliEntry, 'restart'];
|
|
1175
|
+
if (configPath) {
|
|
1176
|
+
args.push('--config', path.resolve(configPath));
|
|
1177
|
+
}
|
|
1178
|
+
const child = spawn(process.execPath, args, {
|
|
1179
|
+
cwd,
|
|
1180
|
+
env: { ...process.env },
|
|
1181
|
+
stdio: 'ignore',
|
|
1182
|
+
});
|
|
1183
|
+
child.once('error', reject);
|
|
1184
|
+
child.once('spawn', () => resolve());
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
function renderJson(value) {
|
|
1188
|
+
return JSON.stringify(value, null, 2);
|
|
1189
|
+
}
|
|
1190
|
+
function isPlainObject(value) {
|
|
1191
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1192
|
+
}
|
|
1193
|
+
function truncateText(text, limit) {
|
|
1194
|
+
return text.length > limit ? `${text.slice(0, limit)}...` : text;
|
|
1195
|
+
}
|
|
1196
|
+
//# sourceMappingURL=server.js.map
|