fuli-context 0.7.6 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +69 -13
  2. package/README.zh-CN.md +60 -14
  3. package/dist/web/assets/{AboutPage-sLV9WChi.js → AboutPage-9CVX91tr.js} +1 -1
  4. package/dist/web/assets/{ConnectionsPage-BWn4ywGJ.js → ConnectionsPage-CWbCVFdv.js} +1 -1
  5. package/dist/web/assets/EmployeeRecruitDialog-CQgNpGJ_.js +1 -0
  6. package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
  7. package/dist/web/assets/EmployeeWorkbenchPage-CWg4WJhr.css +1 -0
  8. package/dist/web/assets/EmployeeWorkbenchPage-CwU4qBgD.js +1 -0
  9. package/dist/web/assets/KnowledgeEditDialog-B6TGAcFH.js +1 -0
  10. package/dist/web/assets/{KnowledgeEditDialog-DWUV_NPp.css → KnowledgeEditDialog-iv-dgUeD.css} +1 -1
  11. package/dist/web/assets/{KnowledgeInspector-CRQWOaos.js → KnowledgeInspector-M2pLY0bR.js} +1 -1
  12. package/dist/web/assets/{KnowledgeOrganizerPage-Dfj3vIpU.js → KnowledgeOrganizerPage-DzeAlk9b.js} +1 -1
  13. package/dist/web/assets/KnowledgePage-BJu9w0fF.js +1 -0
  14. package/dist/web/assets/{KnowledgeWorkspace-ChEB5Bwk.js → KnowledgeWorkspace-CCt8rnBb.js} +1 -1
  15. package/dist/web/assets/{OverviewPage-Cd66M0RT.js → OverviewPage-CZswVqui.js} +1 -1
  16. package/dist/web/assets/{PersonalProfilePage-RCFHL0Wz.js → PersonalProfilePage-f1nSF6kx.js} +1 -1
  17. package/dist/web/assets/PersonalProjectsPage-D__eJf0t.js +1 -0
  18. package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
  19. package/dist/web/assets/ProjectAgentsPage-LrwagMGj.js +11 -0
  20. package/dist/web/assets/{PublicProjectsPage-mstPEjHe.js → PublicProjectsPage-BdC2Qk4n.js} +1 -1
  21. package/dist/web/assets/{ReviewPage-6Y1SmfZy.js → ReviewPage-Cbgl8_Si.js} +1 -1
  22. package/dist/web/assets/SearchableSelect-ERm-k4KI.js +1 -0
  23. package/dist/web/assets/{SettingsPage--LdTMUm7.js → SettingsPage-DWso4x29.js} +1 -1
  24. package/dist/web/assets/{WritingTastePage-CIpKswvs.js → WritingTastePage-CFpCjmIY.js} +1 -1
  25. package/dist/web/assets/catalog-WGLn7ERJ.js +1 -0
  26. package/dist/web/assets/{console-BT1P2Db4.js → console-DQgu0JIA.js} +1 -1
  27. package/dist/web/assets/conversation-launcher-settings-0KtlG_LV.js +1 -0
  28. package/dist/web/assets/{i18n-js6gfc7j.js → i18n-ByRiA3pJ.js} +3 -3
  29. package/dist/web/assets/{identity-CfDfanpa.js → identity-CxdPRMwH.js} +1 -1
  30. package/dist/web/assets/index-JAZe6lfX.js +2 -0
  31. package/dist/web/assets/index-wcXzzggp.css +1 -0
  32. package/dist/web/assets/{model-DJkLAJsg.js → model-Db4-KefQ.js} +1 -1
  33. package/dist/web/assets/{useApi-CROJJdhE-BdSti7Y2.js → useApi-CROJJdhE-CePMjc08.js} +1 -1
  34. package/dist/web/assets/{vue-router-Kk67EaCP.js → vue-router-CzX-hn3P.js} +1 -1
  35. package/dist/web/index.html +7 -6
  36. package/docs/employee-agents.md +99 -0
  37. package/docs/project-agent-memory.md +479 -0
  38. package/graph-provider/fuli_graph/knowledge_audit_models.py +171 -0
  39. package/graph-provider/fuli_graph/model_base.py +5 -0
  40. package/graph-provider/fuli_graph/model_validation.py +32 -0
  41. package/graph-provider/fuli_graph/models.py +26 -170
  42. package/graph-provider/fuli_graph/project_agent_context_models.py +29 -0
  43. package/graph-provider/fuli_graph/project_agent_executor_models.py +35 -7
  44. package/graph-provider/fuli_graph/project_agent_memory_models.py +79 -0
  45. package/graph-provider/fuli_graph/project_agent_memory_routes.py +46 -0
  46. package/graph-provider/fuli_graph/project_agent_models.py +9 -0
  47. package/graph-provider/fuli_graph/project_agent_recruitment_values.py +48 -0
  48. package/graph-provider/fuli_graph/project_agent_routes.py +21 -1
  49. package/graph-provider/fuli_graph/project_agent_task_models.py +217 -29
  50. package/graph-provider/fuli_graph/runtime.py +37 -1
  51. package/graph-provider/fuli_graph/store.py +6 -0
  52. package/graph-provider/fuli_graph/store_project_agent_context.py +65 -0
  53. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +175 -11
  54. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +33 -10
  55. package/graph-provider/fuli_graph/store_project_agent_executors.py +181 -14
  56. package/graph-provider/fuli_graph/store_project_agent_memory.py +174 -0
  57. package/graph-provider/fuli_graph/store_project_agent_task_activity.py +423 -0
  58. package/graph-provider/fuli_graph/store_project_agent_task_persistence.py +152 -0
  59. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +187 -21
  60. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +748 -162
  61. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +230 -18
  62. package/graph-provider/fuli_graph/store_project_agent_tasks.py +261 -468
  63. package/graph-provider/fuli_graph/store_project_agents.py +214 -46
  64. package/graph-provider/fuli_graph/store_task_contexts.py +217 -0
  65. package/graph-provider/fuli_graph/store_transactions.py +40 -0
  66. package/graph-provider/fuli_graph/task_context_models.py +50 -0
  67. package/graph-provider/fuli_graph/task_context_routes.py +31 -0
  68. package/graph-provider/fuli_graph/workflow_observation_models.py +1 -1
  69. package/graph-provider/pyproject.toml +1 -1
  70. package/npm-shrinkwrap.json +8 -8
  71. package/package.json +5 -2
  72. package/skills/capturing-session-knowledge/SKILL.md +18 -6
  73. package/src/adaptive-runtime/executor-pool.js +46 -5
  74. package/src/adaptive-runtime/graph-services.js +10 -3
  75. package/src/adaptive-runtime/lease-client.js +66 -14
  76. package/src/adaptive-runtime/runtime-broker.js +24 -9
  77. package/src/agent-tools/employee-definitions.js +43 -0
  78. package/src/agent-tools/graph-definitions.js +25 -6
  79. package/src/agent-tools/graph-handlers.js +11 -0
  80. package/src/agent-tools/project-agent-definitions.js +35 -4
  81. package/src/agent-tools/project-agent-memory-definitions.js +36 -0
  82. package/src/agent-tools.js +6 -2
  83. package/src/agents/codex/lifecycle-hook.js +107 -0
  84. package/src/agents/codex/lifecycle-hooks.js +231 -0
  85. package/src/agents/cursor/lifecycle-hook.js +133 -0
  86. package/src/agents/cursor/lifecycle-hooks.js +50 -0
  87. package/src/agents/lifecycle-message.js +19 -0
  88. package/src/app/agent-request-context.js +23 -0
  89. package/src/cli/command-registry.js +2 -0
  90. package/src/cli/employee-command.js +17 -0
  91. package/src/cli/remote-mcp-command.js +282 -0
  92. package/src/cli/setup-command.js +5 -0
  93. package/src/cli/update-command.js +6 -43
  94. package/src/cli.js +10 -0
  95. package/src/employees/catalog/jefa.json +21 -0
  96. package/src/employees/install-package.js +77 -0
  97. package/src/employees/management-policy.js +88 -0
  98. package/src/employees/manifest.js +53 -0
  99. package/src/employees/package-registry.js +107 -0
  100. package/src/employees/project-selection.js +50 -0
  101. package/src/employees/recruitment.js +199 -0
  102. package/src/employees/service.js +83 -0
  103. package/src/employees/task-entry.js +39 -0
  104. package/src/graphiti/agent-knowledge-workflows.js +135 -25
  105. package/src/graphiti/federated-application.js +78 -35
  106. package/src/graphiti/project-agent-control-plane.js +20 -3
  107. package/src/graphiti/project-agent-mapping.js +155 -5
  108. package/src/graphiti/project-agent-memory.js +115 -0
  109. package/src/graphiti/project-agent-task-entry.js +126 -0
  110. package/src/graphiti/project-agent-workflows.js +30 -14
  111. package/src/graphiti/provider-client.js +66 -4
  112. package/src/http/api-router.js +30 -5
  113. package/src/http/employee-api-router.js +64 -0
  114. package/src/http/error-mapping.js +4 -0
  115. package/src/http/graph-api-router.js +2 -0
  116. package/src/http/request-policy.js +6 -3
  117. package/src/http/static-handler.js +4 -3
  118. package/src/mcp/create-mcp-server.js +98 -37
  119. package/src/mcp/direct-call.js +38 -0
  120. package/src/mcp/instructions.js +8 -7
  121. package/src/mcp/provider-task-context-registry.js +74 -0
  122. package/src/mcp/remote-http-server.js +761 -0
  123. package/src/mcp/session-id.js +29 -3
  124. package/src/mcp/task-context-registry.js +88 -29
  125. package/src/mcp/tool-annotations.js +10 -2
  126. package/src/mcp/tool-result.js +8 -3
  127. package/src/mcp-server.js +20 -8
  128. package/src/native-runtime/runtime.js +58 -8
  129. package/src/semantic-version.js +42 -0
  130. package/src/server.js +76 -28
  131. package/src/setup/agent-installation-status.js +38 -6
  132. package/src/setup/agents.js +20 -8
  133. package/src/setup/claude-code-config.js +55 -13
  134. package/src/setup/codex-bootstrap.js +11 -10
  135. package/src/setup/codex-config.js +74 -7
  136. package/src/setup/config-backup.js +18 -2
  137. package/src/setup/cursor-config.js +22 -5
  138. package/src/setup/setup.js +30 -22
  139. package/src/storage/json-file.js +11 -3
  140. package/src/system/package-version-checker.js +72 -0
  141. package/src/system/system-service.js +3 -0
  142. package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +0 -1
  143. package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +0 -1
  144. package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +0 -1
  145. package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +0 -1
  146. package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +0 -11
  147. package/dist/web/assets/SearchableSelect-D3HM8YF8.js +0 -1
  148. package/dist/web/assets/conversation-launcher-settings-DrJEotdf.js +0 -1
  149. package/dist/web/assets/index-BnH5mcZx.css +0 -1
  150. package/dist/web/assets/index-GqJf1xqg.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
+ }
@@ -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
  }
@@ -10,6 +10,12 @@ import {
10
10
  import { quoteShellArgument } from './shell-argument.js';
11
11
  import { parseUpdateOptions } from '../setup/options.js';
12
12
  import { runLocalRuntimeCommand } from './local-runtime-command.js';
13
+ import {
14
+ compareSemanticVersions,
15
+ parseSemanticVersion
16
+ } from '../semantic-version.js';
17
+
18
+ export { compareSemanticVersions } from '../semantic-version.js';
13
19
 
14
20
  const LATEST_PACKAGE = `${FULI_PACKAGE_NAME}@latest`;
15
21
 
@@ -161,34 +167,6 @@ export function formatUpdateResult(result) {
161
167
  return `Fuli updated from ${result.previousVersion} to ${result.version}.`;
162
168
  }
163
169
 
164
- export function compareSemanticVersions(left, right) {
165
- const parsedLeft = parseSemanticVersion(left);
166
- const parsedRight = parseSemanticVersion(right);
167
- for (let index = 0; index < 3; index += 1) {
168
- if (parsedLeft.core[index] > parsedRight.core[index]) return 1;
169
- if (parsedLeft.core[index] < parsedRight.core[index]) return -1;
170
- }
171
- if (!parsedLeft.prerelease.length && !parsedRight.prerelease.length) return 0;
172
- if (!parsedLeft.prerelease.length) return 1;
173
- if (!parsedRight.prerelease.length) return -1;
174
- const length = Math.max(parsedLeft.prerelease.length, parsedRight.prerelease.length);
175
- for (let index = 0; index < length; index += 1) {
176
- const leftPart = parsedLeft.prerelease[index];
177
- const rightPart = parsedRight.prerelease[index];
178
- if (leftPart === undefined) return -1;
179
- if (rightPart === undefined) return 1;
180
- if (leftPart === rightPart) continue;
181
- const leftNumeric = /^\d+$/.test(leftPart);
182
- const rightNumeric = /^\d+$/.test(rightPart);
183
- if (leftNumeric && rightNumeric) {
184
- return BigInt(leftPart) > BigInt(rightPart) ? 1 : -1;
185
- }
186
- if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
187
- return leftPart > rightPart ? 1 : -1;
188
- }
189
- return 0;
190
- }
191
-
192
170
  export function resolveGlobalCliPath(globalRoot, platform = process.platform) {
193
171
  const path = platform === 'win32' ? win32 : posix;
194
172
  if (typeof globalRoot !== 'string' || !path.isAbsolute(globalRoot.trim())) return null;
@@ -226,21 +204,6 @@ function queryLatestVersion(npmCommand, runCaptured, env) {
226
204
  return version;
227
205
  }
228
206
 
229
- function parseSemanticVersion(value) {
230
- const text = String(value ?? '').trim();
231
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
232
- .exec(text);
233
- if (!match) throw new TypeError(`Invalid package version: ${text || '<empty>'}`);
234
- const numeric = match.slice(1, 4);
235
- if (numeric.some((part) => part.length > 1 && part.startsWith('0'))) {
236
- throw new TypeError(`Invalid package version: ${text}`);
237
- }
238
- return {
239
- core: numeric.map((part) => BigInt(part)),
240
- prerelease: match[4]?.split('.') ?? []
241
- };
242
- }
243
-
244
207
  function formatSetupRecoveryCommand(args, platform) {
245
208
  const forwarded = args
246
209
  .filter((arg) => arg !== '--yes')
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
+ }