fuli-context 0.7.5 → 0.7.7
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/README.md +69 -13
- package/README.zh-CN.md +60 -14
- package/dist/web/assets/{AboutPage-ByCUor-j.js → AboutPage-BcZeFcHe.js} +1 -1
- package/dist/web/assets/{ConnectionsPage-DkdkZ40Q.js → ConnectionsPage-xAQYl1SQ.js} +1 -1
- package/dist/web/assets/EmployeeRecruitDialog-CbPAmhy7.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-BQvTLoc8.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-DF3EL0Ui.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-UnnBDt0I.js +1 -0
- package/dist/web/assets/{KnowledgeEditDialog-DWUV_NPp.css → KnowledgeEditDialog-iv-dgUeD.css} +1 -1
- package/dist/web/assets/{KnowledgeInspector-CYAjAb9J.js → KnowledgeInspector-BqPJH5Rr.js} +1 -1
- package/dist/web/assets/{KnowledgeOrganizerPage-CV4F54R_.js → KnowledgeOrganizerPage-CyvRflXM.js} +1 -1
- package/dist/web/assets/KnowledgePage-59eczTqe.js +1 -0
- package/dist/web/assets/{KnowledgeWorkspace-z4u5mXOG.js → KnowledgeWorkspace-CfIjhOog.js} +1 -1
- package/dist/web/assets/{OverviewPage-BWOfeS6k.js → OverviewPage-Xyfm5xkZ.js} +1 -1
- package/dist/web/assets/{PersonalProfilePage-c3w4-h7a.js → PersonalProfilePage-DB9mByB3.js} +1 -1
- package/dist/web/assets/PersonalProjectsPage-_-YXVi5O.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-DeeQByPX.js +11 -0
- package/dist/web/assets/{PublicProjectsPage-DdH0oGB3.js → PublicProjectsPage-BEZ1YPs3.js} +1 -1
- package/dist/web/assets/{ReviewPage-BYIpcIHG.js → ReviewPage-CQYGwFBH.js} +1 -1
- package/dist/web/assets/SearchableSelect-D27H0MoP.js +1 -0
- package/dist/web/assets/{SettingsPage-2VkbXgZi.js → SettingsPage-BlEZ03Yi.js} +1 -1
- package/dist/web/assets/{WritingTastePage-BOPQZuva.js → WritingTastePage-DvRt_nmP.js} +1 -1
- package/dist/web/assets/catalog-DFvIaHVS.js +1 -0
- package/dist/web/assets/{console-BlAhujsH.js → console-tOxOl8Vq.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-DuFSpPAo.js +1 -0
- package/dist/web/assets/{i18n-Ci3heuiL.js → i18n-CgBHZthI.js} +3 -3
- package/dist/web/assets/{identity-4_-v6cB7.js → identity-CFo_Uw1H.js} +1 -1
- package/dist/web/assets/index-CJc5ppXH.css +1 -0
- package/dist/web/assets/index-yCi3cz2l.js +2 -0
- package/dist/web/assets/{model-yG0YqUyZ.js → model-BtnVr7fX.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-twhdZArA.js → useApi-CROJJdhE-B6MHm_Av.js} +1 -1
- package/dist/web/assets/{vue-router-C-wot_Di.js → vue-router-CzHm-y-x.js} +1 -1
- package/dist/web/index.html +7 -6
- package/docs/employee-agents.md +99 -0
- package/docs/project-agent-memory.md +479 -0
- package/graph-provider/fuli_graph/knowledge_audit_models.py +171 -0
- package/graph-provider/fuli_graph/model_base.py +5 -0
- package/graph-provider/fuli_graph/model_validation.py +32 -0
- package/graph-provider/fuli_graph/models.py +26 -170
- package/graph-provider/fuli_graph/project_agent_context_models.py +29 -0
- package/graph-provider/fuli_graph/project_agent_coordination_models.py +22 -0
- package/graph-provider/fuli_graph/project_agent_executor_models.py +35 -7
- package/graph-provider/fuli_graph/project_agent_memory_models.py +79 -0
- package/graph-provider/fuli_graph/project_agent_memory_routes.py +46 -0
- package/graph-provider/fuli_graph/project_agent_models.py +9 -0
- package/graph-provider/fuli_graph/project_agent_recruitment_values.py +48 -0
- package/graph-provider/fuli_graph/project_agent_routes.py +50 -1
- package/graph-provider/fuli_graph/project_agent_task_models.py +220 -29
- package/graph-provider/fuli_graph/runtime.py +37 -1
- package/graph-provider/fuli_graph/store.py +6 -0
- package/graph-provider/fuli_graph/store_project_agent_context.py +65 -0
- package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +122 -0
- package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +175 -11
- package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +33 -10
- package/graph-provider/fuli_graph/store_project_agent_executors.py +181 -14
- package/graph-provider/fuli_graph/store_project_agent_memory.py +174 -0
- package/graph-provider/fuli_graph/store_project_agent_task_activity.py +423 -0
- package/graph-provider/fuli_graph/store_project_agent_task_persistence.py +152 -0
- package/graph-provider/fuli_graph/store_project_agent_task_reads.py +187 -21
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +756 -164
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +396 -7
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +265 -465
- package/graph-provider/fuli_graph/store_project_agents.py +214 -46
- package/graph-provider/fuli_graph/store_task_contexts.py +217 -0
- package/graph-provider/fuli_graph/store_transactions.py +40 -0
- package/graph-provider/fuli_graph/task_context_models.py +50 -0
- package/graph-provider/fuli_graph/task_context_routes.py +31 -0
- package/graph-provider/fuli_graph/workflow_observation_models.py +1 -1
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +8 -8
- package/package.json +5 -2
- package/skills/capturing-session-knowledge/SKILL.md +18 -6
- package/src/adaptive-runtime/executor-pool.js +46 -5
- package/src/adaptive-runtime/graph-services.js +10 -3
- package/src/adaptive-runtime/lease-client.js +66 -14
- package/src/adaptive-runtime/runtime-broker.js +24 -9
- package/src/agent-tools/employee-definitions.js +43 -0
- package/src/agent-tools/graph-definitions.js +48 -6
- package/src/agent-tools/graph-handlers.js +15 -0
- package/src/agent-tools/project-agent-definitions.js +35 -4
- package/src/agent-tools/project-agent-memory-definitions.js +36 -0
- package/src/agent-tools.js +6 -2
- package/src/agents/codex/lifecycle-hook.js +107 -0
- package/src/agents/codex/lifecycle-hooks.js +231 -0
- package/src/agents/cursor/lifecycle-hook.js +133 -0
- package/src/agents/cursor/lifecycle-hooks.js +50 -0
- package/src/agents/lifecycle-message.js +19 -0
- package/src/app/agent-request-context.js +23 -0
- package/src/cli/command-registry.js +2 -0
- package/src/cli/employee-command.js +17 -0
- package/src/cli/remote-mcp-command.js +282 -0
- package/src/cli/setup-command.js +5 -0
- package/src/cli.js +10 -0
- package/src/employees/catalog/jefa.json +21 -0
- package/src/employees/install-package.js +77 -0
- package/src/employees/management-policy.js +88 -0
- package/src/employees/manifest.js +53 -0
- package/src/employees/package-registry.js +107 -0
- package/src/employees/project-selection.js +50 -0
- package/src/employees/recruitment.js +199 -0
- package/src/employees/service.js +83 -0
- package/src/employees/task-entry.js +39 -0
- package/src/graphiti/agent-knowledge-workflows.js +135 -25
- package/src/graphiti/federated-application.js +78 -35
- package/src/graphiti/project-agent-control-plane.js +32 -3
- package/src/graphiti/project-agent-mapping.js +175 -4
- package/src/graphiti/project-agent-memory.js +115 -0
- package/src/graphiti/project-agent-task-entry.js +126 -0
- package/src/graphiti/project-agent-workflows.js +49 -14
- package/src/graphiti/provider-client.js +78 -4
- package/src/http/api-router.js +25 -5
- package/src/http/employee-api-router.js +64 -0
- package/src/http/error-mapping.js +4 -0
- package/src/http/graph-api-router.js +14 -0
- package/src/http/request-policy.js +6 -3
- package/src/http/static-handler.js +4 -3
- package/src/mcp/create-mcp-server.js +98 -37
- package/src/mcp/direct-call.js +38 -0
- package/src/mcp/instructions.js +8 -7
- package/src/mcp/provider-task-context-registry.js +74 -0
- package/src/mcp/remote-http-server.js +761 -0
- package/src/mcp/session-id.js +29 -3
- package/src/mcp/task-context-registry.js +88 -29
- package/src/mcp/tool-annotations.js +12 -2
- package/src/mcp/tool-result.js +8 -3
- package/src/mcp-server.js +20 -8
- package/src/native-runtime/runtime.js +58 -8
- package/src/server.js +76 -28
- package/src/setup/agent-installation-status.js +38 -6
- package/src/setup/agents.js +20 -8
- package/src/setup/claude-code-config.js +55 -13
- package/src/setup/codex-bootstrap.js +11 -10
- package/src/setup/codex-config.js +74 -7
- package/src/setup/config-backup.js +18 -2
- package/src/setup/cursor-config.js +22 -5
- package/src/setup/setup.js +30 -22
- package/src/storage/json-file.js +11 -3
- package/dist/web/assets/KnowledgeEditDialog-DSe3Gwep.js +0 -1
- package/dist/web/assets/KnowledgePage-vYYXipOi.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-B7AlLEwf.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-CgftpIPB.css +0 -1
- package/dist/web/assets/ProjectAgentsPage-DTHcL_ox.js +0 -11
- package/dist/web/assets/SearchableSelect-Dz13ClQb.js +0 -1
- package/dist/web/assets/conversation-launcher-settings-DTYxiLJt.js +0 -1
- package/dist/web/assets/index-BnH5mcZx.css +0 -1
- package/dist/web/assets/index-C67Q0JY2.js +0 -2
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
constants as fsConstants,
|
|
4
|
+
fstatSync,
|
|
5
|
+
openSync,
|
|
6
|
+
readFileSync
|
|
7
|
+
} from 'node:fs';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
bindRuntimeLeaseAgentTools,
|
|
12
|
+
createRuntimeLeaseClient
|
|
13
|
+
} from '../adaptive-runtime/lease-client.js';
|
|
14
|
+
import { openFederatedGraphApplication } from '../graphiti/federated-application.js';
|
|
15
|
+
import { resolveGraphRuntimeOptions } from '../graphiti/runtime-config.js';
|
|
16
|
+
import { createRemoteMcpHttpServer } from '../mcp/remote-http-server.js';
|
|
17
|
+
import { normalizeMcpSourceApplication } from '../mcp/session-id.js';
|
|
18
|
+
|
|
19
|
+
const DEFAULT_REMOTE_MCP_PORT = 2728;
|
|
20
|
+
const DEFAULT_REMOTE_MCP_MAX_SESSIONS = 8;
|
|
21
|
+
const DEFAULT_REMOTE_MCP_SESSION_IDLE_TTL_SECONDS = 15 * 60;
|
|
22
|
+
const REMOTE_MCP_OPTIONS = new Set([
|
|
23
|
+
'--personal-project-id', '--bearer-token-file', '--data-dir', '--host', '--port',
|
|
24
|
+
'--source-application', '--allowed-origin', '--allowed-host', '--max-sessions',
|
|
25
|
+
'--session-idle-ttl-seconds'
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export function parseRemoteMcpOptions(args) {
|
|
29
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
30
|
+
const flag = args[index];
|
|
31
|
+
if (!REMOTE_MCP_OPTIONS.has(flag)) {
|
|
32
|
+
throw new TypeError(`Unknown remote MCP option: ${flag}`);
|
|
33
|
+
}
|
|
34
|
+
const value = args[index + 1];
|
|
35
|
+
if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
|
|
36
|
+
throw new TypeError(`Missing value for ${flag}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const personalProjectId = requiredOption(args, '--personal-project-id');
|
|
40
|
+
const bearerTokenFile = requiredOption(args, '--bearer-token-file');
|
|
41
|
+
const dataDir = optionalOption(args, '--data-dir');
|
|
42
|
+
const host = optionalOption(args, '--host') ?? '127.0.0.1';
|
|
43
|
+
if (!['127.0.0.1', '::1', 'localhost'].includes(host)) {
|
|
44
|
+
throw new TypeError('Remote MCP must bind to a loopback host behind an authenticated HTTPS proxy');
|
|
45
|
+
}
|
|
46
|
+
const port = numberOption(args, '--port', DEFAULT_REMOTE_MCP_PORT);
|
|
47
|
+
const sourceApplication = normalizeMcpSourceApplication(
|
|
48
|
+
optionalOption(args, '--source-application') ?? 'claude'
|
|
49
|
+
);
|
|
50
|
+
const allowedOrigin = optionalOption(args, '--allowed-origin');
|
|
51
|
+
const allowedHost = optionalOption(args, '--allowed-host');
|
|
52
|
+
const maxSessions = boundedIntegerOption(
|
|
53
|
+
args, '--max-sessions', DEFAULT_REMOTE_MCP_MAX_SESSIONS, 1, 64
|
|
54
|
+
);
|
|
55
|
+
const sessionIdleTtlSeconds = boundedIntegerOption(
|
|
56
|
+
args,
|
|
57
|
+
'--session-idle-ttl-seconds',
|
|
58
|
+
DEFAULT_REMOTE_MCP_SESSION_IDLE_TTL_SECONDS,
|
|
59
|
+
10,
|
|
60
|
+
24 * 60 * 60
|
|
61
|
+
);
|
|
62
|
+
return {
|
|
63
|
+
personalProjectId,
|
|
64
|
+
bearerTokenFile,
|
|
65
|
+
dataDir,
|
|
66
|
+
port,
|
|
67
|
+
host,
|
|
68
|
+
sourceApplication,
|
|
69
|
+
allowedOrigin,
|
|
70
|
+
allowedHost,
|
|
71
|
+
maxSessions,
|
|
72
|
+
sessionIdleTtlSeconds
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function runRemoteMcpCommand(args, dependencies = {}) {
|
|
77
|
+
const options = parseRemoteMcpOptions(args);
|
|
78
|
+
const env = dependencies.env ?? process.env;
|
|
79
|
+
const runtimeConfigPath = resolveGraphRuntimeOptions([], env, {
|
|
80
|
+
setupPaths: { dataDir: options.dataDir ?? undefined }
|
|
81
|
+
}).runtimeConfigPath;
|
|
82
|
+
const readToken = dependencies.readBearerToken ?? readBearerToken;
|
|
83
|
+
const bearerToken = readToken(options.bearerTokenFile);
|
|
84
|
+
const openApplication = dependencies.openApplication ?? openFederatedGraphApplication;
|
|
85
|
+
const createLeaseClient = dependencies.createLeaseClient ?? createRuntimeLeaseClient;
|
|
86
|
+
const bindLeaseTools = dependencies.bindRuntimeLeaseAgentTools
|
|
87
|
+
?? bindRuntimeLeaseAgentTools;
|
|
88
|
+
let app;
|
|
89
|
+
let leaseClient;
|
|
90
|
+
let remote;
|
|
91
|
+
try {
|
|
92
|
+
app = openApplication({ runtimeConfigPath });
|
|
93
|
+
leaseClient = createLeaseClient({ runtimeConfigPath });
|
|
94
|
+
bindLeaseTools(app, leaseClient);
|
|
95
|
+
await leaseClient.withGraphLease(
|
|
96
|
+
'remote-mcp-project-preflight',
|
|
97
|
+
() => assertPersonalProjectExists(
|
|
98
|
+
app,
|
|
99
|
+
app.config.personal.spaceId,
|
|
100
|
+
options.personalProjectId
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
remote = await (dependencies.createRemoteServer ?? createRemoteMcpHttpServer)({
|
|
104
|
+
app,
|
|
105
|
+
bearerToken,
|
|
106
|
+
personalProjectId: options.personalProjectId,
|
|
107
|
+
personalSpaceId: app.config.personal.spaceId,
|
|
108
|
+
sourceApplication: options.sourceApplication,
|
|
109
|
+
host: options.host,
|
|
110
|
+
port: options.port,
|
|
111
|
+
allowedOrigins: options.allowedOrigin ? [options.allowedOrigin] : [],
|
|
112
|
+
allowedHosts: options.allowedHost ? [options.allowedHost] : [],
|
|
113
|
+
maxSessions: options.maxSessions,
|
|
114
|
+
sessionIdleTtlMs: options.sessionIdleTtlSeconds * 1000,
|
|
115
|
+
withRuntimeLease: (owner, operation) => leaseClient.withGraphLease(owner, operation)
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
await Promise.allSettled([
|
|
119
|
+
Promise.resolve().then(() => leaseClient?.close?.()),
|
|
120
|
+
Promise.resolve().then(() => app?.close?.())
|
|
121
|
+
]);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const signalTarget = dependencies.signalTarget ?? process;
|
|
126
|
+
const writeError = dependencies.writeError ?? ((value) => process.stderr.write(value));
|
|
127
|
+
const reportError = dependencies.reportError ?? ((error) => {
|
|
128
|
+
writeError(`Fuli remote MCP shutdown failed: ${describeError(error)}\n`);
|
|
129
|
+
});
|
|
130
|
+
const setExitCode = dependencies.setExitCode ?? ((code) => { process.exitCode = code; });
|
|
131
|
+
const handlers = new Map();
|
|
132
|
+
let closing;
|
|
133
|
+
const close = () => {
|
|
134
|
+
if (closing) return closing;
|
|
135
|
+
closing = Promise.resolve().then(async () => {
|
|
136
|
+
const failures = [];
|
|
137
|
+
for (const resource of [remote, leaseClient, app]) {
|
|
138
|
+
try {
|
|
139
|
+
await resource.close();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
failures.push(error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (failures.length === 1) throw failures[0];
|
|
145
|
+
if (failures.length > 1) {
|
|
146
|
+
throw new AggregateError(failures, 'Fuli remote MCP shutdown failed');
|
|
147
|
+
}
|
|
148
|
+
}).finally(() => {
|
|
149
|
+
for (const [signal, handler] of handlers) signalTarget.off(signal, handler);
|
|
150
|
+
});
|
|
151
|
+
return closing;
|
|
152
|
+
};
|
|
153
|
+
const write = dependencies.write ?? ((value) => process.stdout.write(`${value}\n`));
|
|
154
|
+
try {
|
|
155
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
156
|
+
const handler = () => {
|
|
157
|
+
void close().catch((error) => {
|
|
158
|
+
setExitCode(1);
|
|
159
|
+
reportError(error);
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
handlers.set(signal, handler);
|
|
163
|
+
signalTarget.once(signal, handler);
|
|
164
|
+
}
|
|
165
|
+
write([
|
|
166
|
+
`Fuli remote MCP listening on ${remote.url}/mcp`,
|
|
167
|
+
`Bound project: ${options.personalProjectId}`,
|
|
168
|
+
'This listener is loopback-only. Publish it only through an authenticated HTTPS OAuth proxy; never expose the local Provider or Neo4j ports.'
|
|
169
|
+
].join('\n'));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
try {
|
|
172
|
+
await close();
|
|
173
|
+
} catch (cleanupError) {
|
|
174
|
+
const cleanupFailures = cleanupError instanceof AggregateError
|
|
175
|
+
? cleanupError.errors
|
|
176
|
+
: [cleanupError];
|
|
177
|
+
throw new AggregateError(
|
|
178
|
+
[error, ...cleanupFailures],
|
|
179
|
+
'Fuli remote MCP startup finalization failed'
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
return { status: 'running', url: `${remote.url}/mcp`, close, server: remote.server };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function describeError(error) {
|
|
188
|
+
if (error instanceof AggregateError) {
|
|
189
|
+
return error.errors.map(describeError).join('; ');
|
|
190
|
+
}
|
|
191
|
+
return error?.message ?? String(error);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function assertPersonalProjectExists(app, personalSpaceId, personalProjectId) {
|
|
195
|
+
const projects = await app.listPersonalProjects({ personalSpaceId });
|
|
196
|
+
if (!Array.isArray(projects) || !projects.some(project => [
|
|
197
|
+
project?.project_id,
|
|
198
|
+
project?.projectId,
|
|
199
|
+
project?.personal_project_id,
|
|
200
|
+
project?.personalProjectId
|
|
201
|
+
].includes(personalProjectId))) {
|
|
202
|
+
throw new TypeError(
|
|
203
|
+
`Remote MCP project ${personalProjectId} does not exist in the configured personal space`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function readBearerToken(path, fileSystem = {}) {
|
|
209
|
+
const open = fileSystem.openSync ?? openSync;
|
|
210
|
+
const fstat = fileSystem.fstatSync ?? fstatSync;
|
|
211
|
+
const readFile = fileSystem.readFileSync ?? readFileSync;
|
|
212
|
+
const close = fileSystem.closeSync ?? closeSync;
|
|
213
|
+
const constants = fileSystem.constants ?? fsConstants;
|
|
214
|
+
const absolute = resolve(path);
|
|
215
|
+
let descriptor;
|
|
216
|
+
try {
|
|
217
|
+
descriptor = open(
|
|
218
|
+
absolute,
|
|
219
|
+
constants.O_RDONLY
|
|
220
|
+
| (constants.O_NOFOLLOW ?? 0)
|
|
221
|
+
| (constants.O_NONBLOCK ?? 0)
|
|
222
|
+
);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (error?.code === 'ELOOP' || error?.code === 'EMLINK') {
|
|
225
|
+
throw new TypeError('Remote MCP bearer token path must not be a symbolic link');
|
|
226
|
+
}
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const metadata = fstat(descriptor);
|
|
231
|
+
if (!metadata.isFile()) throw new TypeError('Remote MCP bearer token path must be a regular file');
|
|
232
|
+
if (metadata.size > 4096) {
|
|
233
|
+
throw new TypeError('Remote MCP bearer token file must not exceed 4096 bytes');
|
|
234
|
+
}
|
|
235
|
+
if (process.platform !== 'win32' && (metadata.mode & 0o077) !== 0) {
|
|
236
|
+
throw new TypeError('Remote MCP bearer token file must be readable only by its owner (chmod 600)');
|
|
237
|
+
}
|
|
238
|
+
const token = readFile(descriptor, 'utf8').trim();
|
|
239
|
+
if (!/^[\x21-\x7E]{24,512}$/.test(token)) {
|
|
240
|
+
throw new TypeError(
|
|
241
|
+
'Remote MCP bearer token must contain 24 to 512 visible ASCII characters without spaces'
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return token;
|
|
245
|
+
} finally {
|
|
246
|
+
close(descriptor);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function requiredOption(args, flag) {
|
|
251
|
+
const value = optionalOption(args, flag);
|
|
252
|
+
if (!value) throw new TypeError(`Missing ${flag}`);
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function optionalOption(args, flag) {
|
|
257
|
+
const index = args.indexOf(flag);
|
|
258
|
+
if (index === -1) return null;
|
|
259
|
+
if (args.indexOf(flag, index + 1) !== -1) throw new TypeError(`Duplicate ${flag}`);
|
|
260
|
+
const value = args[index + 1];
|
|
261
|
+
if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
|
|
262
|
+
throw new TypeError(`Missing value for ${flag}`);
|
|
263
|
+
}
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function numberOption(args, flag, fallback) {
|
|
268
|
+
return boundedIntegerOption(args, flag, fallback, 0, 65_535);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function boundedIntegerOption(args, flag, fallback, minimum, maximum) {
|
|
272
|
+
const value = optionalOption(args, flag);
|
|
273
|
+
if (value === null) return fallback;
|
|
274
|
+
if (!/^\d+$/.test(value)) {
|
|
275
|
+
throw new TypeError(`${flag} must be an integer from ${minimum} to ${maximum}`);
|
|
276
|
+
}
|
|
277
|
+
const parsed = Number(value);
|
|
278
|
+
if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
|
|
279
|
+
throw new TypeError(`${flag} must be an integer from ${minimum} to ${maximum}`);
|
|
280
|
+
}
|
|
281
|
+
return parsed;
|
|
282
|
+
}
|
package/src/cli/setup-command.js
CHANGED
|
@@ -96,6 +96,10 @@ export function formatSetupResult(result, plan) {
|
|
|
96
96
|
}
|
|
97
97
|
lines.push('Knowledge storage: Graphiti / Neo4j');
|
|
98
98
|
for (const agent of result.agents) {
|
|
99
|
+
if (agent.status === 'partial') {
|
|
100
|
+
lines.push(`${agent.label}: partially configured; retry setup to finish installation`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
99
103
|
if (agent.status !== 'connected') {
|
|
100
104
|
lines.push(`${agent.label}: connection failed; retry setup later`);
|
|
101
105
|
continue;
|
|
@@ -103,6 +107,7 @@ export function formatSetupResult(result, plan) {
|
|
|
103
107
|
lines.push(agent.newTaskRequired
|
|
104
108
|
? `${agent.label}: connected; create or reopen a task to load the new configuration`
|
|
105
109
|
: `${agent.label}: connected`);
|
|
110
|
+
for (const step of agent.nextSteps ?? []) lines.push(`${agent.label}: ${step}`);
|
|
106
111
|
}
|
|
107
112
|
return lines.join('\n');
|
|
108
113
|
}
|
package/src/cli.js
CHANGED
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
printHelp
|
|
8
8
|
} from './cli/command-registry.js';
|
|
9
9
|
import { runLocalRuntimeCommand } from './cli/local-runtime-command.js';
|
|
10
|
+
import { runRemoteMcpCommand } from './cli/remote-mcp-command.js';
|
|
10
11
|
import { runGraphDataCommand } from './cli/graph-data-command.js';
|
|
12
|
+
import { runEmployeeCommand } from './cli/employee-command.js';
|
|
11
13
|
import { runSetupCommand } from './cli/setup-command.js';
|
|
12
14
|
import { runUninstallCommand } from './cli/uninstall-command.js';
|
|
13
15
|
import { runUpdateCommand } from './cli/update-command.js';
|
|
@@ -35,10 +37,18 @@ export async function main(argv = process.argv.slice(2), env = process.env) {
|
|
|
35
37
|
const [action, ...graphArgs] = commandArgs;
|
|
36
38
|
return runGraphDataCommand(action, graphArgs, { env });
|
|
37
39
|
}
|
|
40
|
+
if (command === 'employee') {
|
|
41
|
+
assertSupportedNodeVersion();
|
|
42
|
+
return runEmployeeCommand(commandArgs, { env });
|
|
43
|
+
}
|
|
38
44
|
if (command === 'connect-workspace') {
|
|
39
45
|
assertSupportedNodeVersion();
|
|
40
46
|
return runWorkspaceConnectionCommand(commandArgs, { env });
|
|
41
47
|
}
|
|
48
|
+
if (command === 'remote-mcp') {
|
|
49
|
+
assertSupportedNodeVersion();
|
|
50
|
+
return runRemoteMcpCommand(commandArgs, { env });
|
|
51
|
+
}
|
|
42
52
|
if (command === 'uninstall') {
|
|
43
53
|
await runUninstallCommand(commandArgs, { env });
|
|
44
54
|
return;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "jefa",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"name": "Jefa",
|
|
6
|
+
"role": "项目经理",
|
|
7
|
+
"description": "把目标拆成任务,整理看板与进展,留待你确认。",
|
|
8
|
+
"occupationEmoji": "🧭",
|
|
9
|
+
"defaultProjectScope": "all",
|
|
10
|
+
"taskEntry": { "boardTool": "read_board", "titleTool": "prepare_session_title" },
|
|
11
|
+
"capabilities": ["项目规划", "任务拆解", "看板读写", "迭代复盘"],
|
|
12
|
+
"workKinds": ["project_management", "planning", "review"],
|
|
13
|
+
"initialPreferences": [
|
|
14
|
+
"先读取当前项目的看板,再创建或修改任务;避免重复事项。",
|
|
15
|
+
"修改任务时携带读取到的 updatedAt 版本;冲突后先重新读取。",
|
|
16
|
+
"Agent 认为完成时只移入待确认,最终完成由用户决定。",
|
|
17
|
+
"不推测未上报的模型、token 或执行耗时,不自动扩大项目权限。"
|
|
18
|
+
],
|
|
19
|
+
"permissions": ["board.read", "board.write", "session.title"],
|
|
20
|
+
"runtime": { "apiVersion": 1, "entry": "runtime/index.mjs", "webRoot": "web" }
|
|
21
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync,
|
|
3
|
+
readFileSync, readdirSync, renameSync, rmSync, writeFileSync
|
|
4
|
+
} from 'node:fs';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { EmployeeError, parseEmployeeManifest } from './manifest.js';
|
|
8
|
+
import { confinedPath, employeeDirectories } from './package-registry.js';
|
|
9
|
+
|
|
10
|
+
// Installation is an explicit local CLI action. The HTTP/MCP catalog never
|
|
11
|
+
// downloads code, follows a user-supplied executable URL, or executes installers.
|
|
12
|
+
export function installEmployeePackage({ sourceDirectory, runtimeConfigPath, replace = false }) {
|
|
13
|
+
const source = resolve(sourceDirectory);
|
|
14
|
+
const digest = packageDigest(source);
|
|
15
|
+
const manifest = parseEmployeeManifest(JSON.parse(readFileSync(join(source, 'employee.json'), 'utf8')));
|
|
16
|
+
if (manifest.runtime) {
|
|
17
|
+
confinedPath(source, manifest.runtime.entry);
|
|
18
|
+
confinedPath(source, `${manifest.runtime.webRoot}/index.html`);
|
|
19
|
+
}
|
|
20
|
+
const { packageDirectory } = employeeDirectories(runtimeConfigPath);
|
|
21
|
+
mkdirSync(packageDirectory, { recursive: true });
|
|
22
|
+
const target = join(packageDirectory, manifest.id);
|
|
23
|
+
if (existsSync(target)) {
|
|
24
|
+
// Verify the actual installed files, not an old receipt that may outlive an edit.
|
|
25
|
+
let installedDigest;
|
|
26
|
+
try { installedDigest = packageDigest(target); } catch { /* Explicit replacement is required for a damaged package. */ }
|
|
27
|
+
if (installedDigest === digest) {
|
|
28
|
+
return { id: manifest.id, version: manifest.version, idempotent: true, backupCreated: false };
|
|
29
|
+
}
|
|
30
|
+
if (!replace) throw new EmployeeError('Employee package already exists; use --replace to keep a backup and update it', 409, 'package_exists');
|
|
31
|
+
}
|
|
32
|
+
const staging = mkdtempSync(join(packageDirectory, '.install-'));
|
|
33
|
+
let backup;
|
|
34
|
+
try {
|
|
35
|
+
cpSync(source, join(staging, 'package'), { recursive: true, dereference: false });
|
|
36
|
+
if (packageDigest(join(staging, 'package')) !== digest) throw new TypeError('Employee package changed while installing');
|
|
37
|
+
writeFileSync(join(staging, 'package', '.installation.json'), JSON.stringify({
|
|
38
|
+
schemaVersion: 1, digest, installedAt: new Date().toISOString()
|
|
39
|
+
}));
|
|
40
|
+
if (existsSync(target)) {
|
|
41
|
+
backup = `${target}.backup-${createHash('sha256').update(staging).digest('hex').slice(0, 12)}`;
|
|
42
|
+
renameSync(target, backup);
|
|
43
|
+
}
|
|
44
|
+
try { renameSync(join(staging, 'package'), target); }
|
|
45
|
+
catch (error) { if (backup) renameSync(backup, target); throw error; }
|
|
46
|
+
} finally {
|
|
47
|
+
rmSync(staging, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
return { id: manifest.id, version: manifest.version, idempotent: false, backupCreated: Boolean(backup) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function packageDigest(root) {
|
|
53
|
+
let bytes = 0;
|
|
54
|
+
let files = 0;
|
|
55
|
+
const hash = createHash('sha256');
|
|
56
|
+
function walk(directory, prefix = '') {
|
|
57
|
+
if (lstatSync(directory).isSymbolicLink()) throw new TypeError('Employee packages may not contain symlinks');
|
|
58
|
+
for (const name of readdirSync(directory).sort()) {
|
|
59
|
+
if (name === '.env' || name.startsWith('.env.') || ['.git', 'node_modules'].includes(name)) {
|
|
60
|
+
throw new TypeError('Employee packages must contain only built artifacts, not environments or repositories');
|
|
61
|
+
}
|
|
62
|
+
const file = join(directory, name);
|
|
63
|
+
const stat = lstatSync(file);
|
|
64
|
+
if (stat.isSymbolicLink()) throw new TypeError('Employee packages may not contain symlinks');
|
|
65
|
+
// Only the generated root receipt is metadata; nested names are package content.
|
|
66
|
+
if (name === '.installation.json' && prefix === '' && stat.isFile()) continue;
|
|
67
|
+
if (stat.isDirectory()) { walk(file, `${prefix}${name}/`); continue; }
|
|
68
|
+
if (!stat.isFile()) throw new TypeError('Unsupported employee package entry');
|
|
69
|
+
bytes += stat.size;
|
|
70
|
+
files += 1;
|
|
71
|
+
if (bytes > 64 * 1024 * 1024 || files > 4096) throw new TypeError('Employee package exceeds the installation budget');
|
|
72
|
+
hash.update(`${prefix}${name}\0`).update(readFileSync(file)).update('\0');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
walk(root);
|
|
76
|
+
return hash.digest('hex');
|
|
77
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { EmployeeError } from './manifest.js';
|
|
6
|
+
|
|
7
|
+
const projectId = z.string().min(1).max(256);
|
|
8
|
+
export const employeeManagementSchema = z.object({
|
|
9
|
+
mode: z.enum(['selected', 'all']),
|
|
10
|
+
projectIds: z.array(projectId).max(500).default([]),
|
|
11
|
+
excludedProjectIds: z.array(projectId).max(500).default([]),
|
|
12
|
+
titleMode: z.enum(['off', 'suggest', 'auto']).default('suggest'),
|
|
13
|
+
titleStyle: z.enum(['text', 'emoji']).default('emoji'),
|
|
14
|
+
}).strict().superRefine((value, context) => {
|
|
15
|
+
if ((value.mode === 'all' && value.projectIds.length) ||
|
|
16
|
+
(value.mode === 'selected' && value.excludedProjectIds.length)) {
|
|
17
|
+
context.addIssue({ code: 'custom', message: 'Use selected project IDs OR all projects with exclusions.' });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function parseEmployeeManagement(value) {
|
|
22
|
+
const result = employeeManagementSchema.safeParse(value);
|
|
23
|
+
if (!result.success) throw new TypeError('Invalid employee management policy');
|
|
24
|
+
return { ...result.data, projectIds: [...new Set(result.data.projectIds)].sort(),
|
|
25
|
+
excludedProjectIds: [...new Set(result.data.excludedProjectIds)].sort() };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Employee policy is local host configuration, not a public project fact or a
|
|
29
|
+
// Provider model/executor policy. SQLite CAS also protects independent MCP hosts.
|
|
30
|
+
export class EmployeeManagementStore {
|
|
31
|
+
constructor(path = ':memory:') {
|
|
32
|
+
if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true });
|
|
33
|
+
this.database = new DatabaseSync(path);
|
|
34
|
+
this.database.exec(`PRAGMA busy_timeout = 5000;
|
|
35
|
+
CREATE TABLE IF NOT EXISTS employee_management (
|
|
36
|
+
space_id TEXT NOT NULL, template_id TEXT NOT NULL,
|
|
37
|
+
revision INTEGER NOT NULL, policy TEXT NOT NULL,
|
|
38
|
+
PRIMARY KEY (space_id, template_id)
|
|
39
|
+
)`);
|
|
40
|
+
}
|
|
41
|
+
read(spaceId, templateId) {
|
|
42
|
+
const row = this.database.prepare('SELECT revision, policy FROM employee_management WHERE space_id = ? AND template_id = ?').get(spaceId, templateId);
|
|
43
|
+
return row ? { revision: row.revision, ...parseEmployeeManagement(JSON.parse(row.policy)) } : null;
|
|
44
|
+
}
|
|
45
|
+
write(spaceId, templateId, policy, expectedRevision) {
|
|
46
|
+
const parsed = parseEmployeeManagement(policy);
|
|
47
|
+
this.database.exec('BEGIN IMMEDIATE');
|
|
48
|
+
try {
|
|
49
|
+
const current = this.read(spaceId, templateId);
|
|
50
|
+
if ((current?.revision ?? 0) !== expectedRevision) {
|
|
51
|
+
throw new EmployeeError('Employee policy changed; reload before saving', 409, 'assignment_scope_conflict');
|
|
52
|
+
}
|
|
53
|
+
const revision = expectedRevision + 1;
|
|
54
|
+
this.database.prepare(`INSERT INTO employee_management (space_id, template_id, revision, policy) VALUES (?, ?, ?, ?)
|
|
55
|
+
ON CONFLICT(space_id, template_id) DO UPDATE SET revision = excluded.revision, policy = excluded.policy`)
|
|
56
|
+
.run(spaceId, templateId, revision, JSON.stringify(parsed));
|
|
57
|
+
this.database.exec('COMMIT');
|
|
58
|
+
return { ...parsed, revision };
|
|
59
|
+
} catch (error) { this.database.exec('ROLLBACK'); throw error; }
|
|
60
|
+
}
|
|
61
|
+
close() { if (!this.closed) { this.closed = true; this.database.close(); } }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function managementFor(manifest, agent, stored) {
|
|
65
|
+
if (stored) {
|
|
66
|
+
if (stored.mode !== 'all') return stored;
|
|
67
|
+
// Ending the last assignment is an explicit prohibition too. Keep that
|
|
68
|
+
// veto until a scope edit explicitly reselects/reassigns the project.
|
|
69
|
+
const history = agent?.assignments ?? [];
|
|
70
|
+
const activeIds = new Set(history.filter(a => a.status === 'active').map(a => a.personalProjectId));
|
|
71
|
+
const endedIds = history.filter(a => a.status === 'ended' && !activeIds.has(a.personalProjectId)).map(a => a.personalProjectId);
|
|
72
|
+
return { ...stored, excludedProjectIds: [...new Set([...stored.excludedProjectIds, ...endedIds])].sort() };
|
|
73
|
+
}
|
|
74
|
+
// Upgrades preserve existing authorization. Only a new recruitment uses the
|
|
75
|
+
// template default; an existing employee opts in by saving the new all rule.
|
|
76
|
+
return { revision: 0, mode: agent ? 'selected' : manifest.defaultProjectScope ?? 'selected',
|
|
77
|
+
projectIds: [...new Set((agent?.assignments ?? []).filter(a => a.status === 'active').map(a => a.personalProjectId))],
|
|
78
|
+
excludedProjectIds: [], titleMode: manifest.taskEntry ? 'auto' : 'suggest', titleStyle: 'emoji' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function policyAllowsProject(policy, id) {
|
|
82
|
+
return policy.mode === 'all' ? !policy.excludedProjectIds.includes(id) : policy.projectIds.includes(id);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function managementWithoutRevision(policy) {
|
|
86
|
+
const { revision: _revision, ...value } = policy;
|
|
87
|
+
return parseEmployeeManagement(value);
|
|
88
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ApplicationError } from '../app/application-error.js';
|
|
3
|
+
|
|
4
|
+
const identifier = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/);
|
|
5
|
+
const toolIdentifier = z.string().regex(/^[a-z][a-z0-9_]{0,63}$/);
|
|
6
|
+
const relativeFile = z.string().min(1).max(160).refine(
|
|
7
|
+
(value) => !value.startsWith('/') && !value.includes('\\') &&
|
|
8
|
+
!value.split('/').some((part) => !part || part === '.' || part === '..') &&
|
|
9
|
+
!value.includes(':'),
|
|
10
|
+
'Employee package paths must stay inside the package'
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export const employeeManifestSchema = z.object({
|
|
14
|
+
schemaVersion: z.literal(1),
|
|
15
|
+
id: identifier,
|
|
16
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
17
|
+
name: z.string().trim().min(1).max(100),
|
|
18
|
+
role: z.string().trim().min(1).max(100),
|
|
19
|
+
description: z.string().trim().min(1).max(1000),
|
|
20
|
+
occupationEmoji: z.string().min(1).max(32),
|
|
21
|
+
capabilities: z.array(z.string().min(1).max(180)).max(24),
|
|
22
|
+
workKinds: z.array(z.string().min(1).max(180)).max(24),
|
|
23
|
+
initialPreferences: z.array(z.string().min(1).max(512)).max(24),
|
|
24
|
+
permissions: z.array(z.string().regex(/^[a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)+$/)).max(24),
|
|
25
|
+
defaultProjectScope: z.enum(['selected', 'all']).optional(),
|
|
26
|
+
taskEntry: z.object({
|
|
27
|
+
boardTool: toolIdentifier,
|
|
28
|
+
titleTool: toolIdentifier.optional(),
|
|
29
|
+
}).strict().optional(),
|
|
30
|
+
runtime: z.object({
|
|
31
|
+
apiVersion: z.literal(1),
|
|
32
|
+
entry: relativeFile,
|
|
33
|
+
webRoot: relativeFile
|
|
34
|
+
}).strict().nullable()
|
|
35
|
+
}).strict();
|
|
36
|
+
|
|
37
|
+
export function parseEmployeeManifest(value) {
|
|
38
|
+
const parsed = employeeManifestSchema.safeParse(value);
|
|
39
|
+
if (!parsed.success) throw new TypeError('Invalid employee package manifest');
|
|
40
|
+
return parsed.data;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const employeeAgentId = (id) => `employee.${identifier.parse(id)}`;
|
|
44
|
+
export const employeeCapability = (id) => `fuli.employee:${identifier.parse(id)}`;
|
|
45
|
+
|
|
46
|
+
export class EmployeeError extends ApplicationError {
|
|
47
|
+
constructor(message, status = 400, code = 'employee_error') {
|
|
48
|
+
super(code, message);
|
|
49
|
+
this.name = 'EmployeeError';
|
|
50
|
+
this.status = status;
|
|
51
|
+
this.code = code;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import { EmployeeError, parseEmployeeManifest } from './manifest.js';
|
|
5
|
+
|
|
6
|
+
const builtinRoot = fileURLToPath(new URL('./catalog/', import.meta.url));
|
|
7
|
+
|
|
8
|
+
export function createEmployeePackageRegistry({ packageDirectory, dataDirectory, catalogDirectory = builtinRoot }) {
|
|
9
|
+
const runtimes = new Map();
|
|
10
|
+
let closed = false;
|
|
11
|
+
|
|
12
|
+
function catalog() {
|
|
13
|
+
const entries = new Map();
|
|
14
|
+
for (const file of readdirSync(catalogDirectory).filter((name) => name.endsWith('.json'))) {
|
|
15
|
+
const manifest = readManifest(join(catalogDirectory, file));
|
|
16
|
+
entries.set(manifest.id, { manifest, root: null, runtimeStatus: manifest.runtime ? 'install_required' : 'not_required' });
|
|
17
|
+
}
|
|
18
|
+
if (packageDirectory && existsSync(packageDirectory)) {
|
|
19
|
+
for (const name of readdirSync(packageDirectory)) {
|
|
20
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) continue;
|
|
21
|
+
const root = join(packageDirectory, name);
|
|
22
|
+
try {
|
|
23
|
+
if (!statSync(root).isDirectory()) continue;
|
|
24
|
+
confinedPath(packageDirectory, name);
|
|
25
|
+
const manifest = readManifest(confinedPath(root, 'employee.json'));
|
|
26
|
+
if (manifest.id !== name) continue;
|
|
27
|
+
if (manifest.runtime) {
|
|
28
|
+
confinedPath(root, manifest.runtime.entry);
|
|
29
|
+
confinedPath(root, `${manifest.runtime.webRoot}/index.html`);
|
|
30
|
+
}
|
|
31
|
+
entries.set(name, { manifest, root, runtimeStatus: manifest.runtime ? 'ready' : 'not_required' });
|
|
32
|
+
} catch {
|
|
33
|
+
const previous = entries.get(name);
|
|
34
|
+
if (previous) entries.set(name, { ...previous, runtimeStatus: 'unavailable' });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return [...entries.values()];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function get(id) {
|
|
42
|
+
const entry = catalog().find(({ manifest }) => manifest.id === id);
|
|
43
|
+
if (!entry) throw new EmployeeError('Employee template not found', 404, 'template_not_found');
|
|
44
|
+
return entry;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function runtime(id) {
|
|
48
|
+
if (closed) throw new EmployeeError('Employee runtime is closed', 503, 'runtime_closed');
|
|
49
|
+
const entry = get(id);
|
|
50
|
+
if (entry.runtimeStatus !== 'ready' || !entry.manifest.runtime) {
|
|
51
|
+
throw new EmployeeError('Employee workbench package is not installed or unavailable', 503, 'runtime_unavailable');
|
|
52
|
+
}
|
|
53
|
+
if (!runtimes.has(id)) {
|
|
54
|
+
const loading = (async () => {
|
|
55
|
+
const module = await import(pathToFileURL(confinedPath(entry.root, entry.manifest.runtime.entry)).href);
|
|
56
|
+
if (typeof module.createEmployeeRuntime !== 'function') throw new TypeError('Invalid employee runtime export');
|
|
57
|
+
const instance = await module.createEmployeeRuntime({
|
|
58
|
+
dataDirectory: resolve(dataDirectory, id),
|
|
59
|
+
packageDirectory: entry.root,
|
|
60
|
+
manifest: entry.manifest
|
|
61
|
+
});
|
|
62
|
+
if (!instance || !['handleHttp', 'callTool', 'describeTools', 'close'].every(
|
|
63
|
+
(name) => typeof instance[name] === 'function'
|
|
64
|
+
)) {
|
|
65
|
+
instance?.close?.();
|
|
66
|
+
throw new TypeError('Employee runtime does not implement API version 1');
|
|
67
|
+
}
|
|
68
|
+
if (closed) { await instance.close(); throw new Error('Employee runtime is closed'); }
|
|
69
|
+
return instance;
|
|
70
|
+
})().catch(() => {
|
|
71
|
+
runtimes.delete(id);
|
|
72
|
+
throw new EmployeeError('Employee runtime could not be loaded', 503, 'runtime_unavailable');
|
|
73
|
+
});
|
|
74
|
+
runtimes.set(id, loading);
|
|
75
|
+
}
|
|
76
|
+
return runtimes.get(id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
catalog, get, runtime,
|
|
81
|
+
async close() {
|
|
82
|
+
closed = true;
|
|
83
|
+
await Promise.allSettled([...runtimes.values()].map(async (entry) => (await entry).close()));
|
|
84
|
+
runtimes.clear();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function confinedPath(root, file) {
|
|
90
|
+
const resolvedRoot = realpathSync(root);
|
|
91
|
+
const resolvedFile = realpathSync(resolve(root, file));
|
|
92
|
+
const local = relative(resolvedRoot, resolvedFile);
|
|
93
|
+
if (local === '..' || local.startsWith(`..${sep}`) || isAbsolute(local)) {
|
|
94
|
+
throw new TypeError('Employee package path escapes its installation');
|
|
95
|
+
}
|
|
96
|
+
return resolvedFile;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readManifest(file) {
|
|
100
|
+
if (statSync(file).size > 32_768) throw new TypeError('Employee manifest is too large');
|
|
101
|
+
return parseEmployeeManifest(JSON.parse(readFileSync(file, 'utf8')));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function employeeDirectories(runtimeConfigPath) {
|
|
105
|
+
const root = join(dirname(runtimeConfigPath), 'employees');
|
|
106
|
+
return { packageDirectory: join(root, 'packages'), dataDirectory: join(root, 'data') };
|
|
107
|
+
}
|