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
package/src/server.js
CHANGED
|
@@ -51,22 +51,52 @@ export async function createServer(options = {}) {
|
|
|
51
51
|
closeApplicationOnShutdown
|
|
52
52
|
});
|
|
53
53
|
const application = runtime.application;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
54
|
+
let externalKnowledge;
|
|
55
|
+
let connectedKnowledge;
|
|
56
|
+
let system = null;
|
|
57
|
+
let resourcesClosing = null;
|
|
58
|
+
function closeResources() {
|
|
59
|
+
resourcesClosing ??= (async () => {
|
|
60
|
+
const failures = [];
|
|
61
|
+
for (const close of [() => system?.close?.(), () => runtime.close()]) {
|
|
62
|
+
try { await close(); } catch (error) { failures.push(error); }
|
|
63
|
+
}
|
|
64
|
+
if (failures.length === 1) throw failures[0];
|
|
65
|
+
if (failures.length > 1) {
|
|
66
|
+
throw new AggregateError(failures, 'Server resource shutdown failed');
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
return resourcesClosing;
|
|
70
|
+
}
|
|
71
|
+
async function failStartup(error) {
|
|
72
|
+
try {
|
|
73
|
+
await closeResources();
|
|
74
|
+
} catch (cleanupError) {
|
|
75
|
+
throw new AggregateError([error, cleanupError],
|
|
76
|
+
`Server startup failed: ${error.message}; resource shutdown also failed`, { cause: error });
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
externalKnowledge = options.externalKnowledge ?? application.externalKnowledge ?? null;
|
|
82
|
+
connectedKnowledge = options.connectedKnowledge ?? application.connectedKnowledge ?? null;
|
|
83
|
+
system = options.system ?? (!app
|
|
84
|
+
? createSystemService({
|
|
85
|
+
paths: resolveSetupPaths({
|
|
86
|
+
dataDir: dirname(localOptions.runtimeConfigPath),
|
|
87
|
+
packageRoot: PACKAGE_ROOT
|
|
88
|
+
}),
|
|
89
|
+
packageRoot: PACKAGE_ROOT,
|
|
90
|
+
activePort: port,
|
|
91
|
+
activeLan: lanEnabled,
|
|
92
|
+
executorAdapters: options.executorAdapters
|
|
93
|
+
})
|
|
94
|
+
: null);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// The owned application can already hold local databases before system
|
|
97
|
+
// construction or configuration validation fails, even without a listener.
|
|
98
|
+
await failStartup(error);
|
|
99
|
+
}
|
|
70
100
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
71
101
|
let authority = null;
|
|
72
102
|
let allowedLanAuthorities = [];
|
|
@@ -82,6 +112,12 @@ export async function createServer(options = {}) {
|
|
|
82
112
|
lanAuthorities: allowedLanAuthorities,
|
|
83
113
|
lanAccessToken: lanToken
|
|
84
114
|
}).catch((error) => {
|
|
115
|
+
if (response.headersSent || response.destroyed) {
|
|
116
|
+
// A post-response lease/storage failure cannot be converted into a second response.
|
|
117
|
+
if (!response.writableEnded) response.destroy();
|
|
118
|
+
console.error('Request failed after its response was committed or disconnected');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
85
121
|
const mapped = mapHttpError(error);
|
|
86
122
|
sendJson(response, mapped.status, mapped.body);
|
|
87
123
|
});
|
|
@@ -90,15 +126,13 @@ export async function createServer(options = {}) {
|
|
|
90
126
|
try {
|
|
91
127
|
await listenServer(server, port, lanEnabled ? '0.0.0.0' : '127.0.0.1');
|
|
92
128
|
} catch (error) {
|
|
93
|
-
|
|
94
|
-
throw error;
|
|
129
|
+
await failStartup(error);
|
|
95
130
|
}
|
|
96
131
|
const address = server.address();
|
|
97
132
|
if (isBlockedPort(address.port)) {
|
|
98
133
|
await new Promise((resolveClose) => server.close(resolveClose));
|
|
99
134
|
if (Number(port) === 0) continue;
|
|
100
|
-
|
|
101
|
-
throw new Error(`Port ${address.port} is blocked for local fetch`);
|
|
135
|
+
await failStartup(new Error(`Port ${address.port} is blocked for local fetch`));
|
|
102
136
|
}
|
|
103
137
|
authority = localServerAuthority(address);
|
|
104
138
|
allowedLanAuthorities = lanEnabled
|
|
@@ -106,24 +140,33 @@ export async function createServer(options = {}) {
|
|
|
106
140
|
: [];
|
|
107
141
|
if (!authority) {
|
|
108
142
|
await new Promise((resolveClose) => server.close(resolveClose));
|
|
109
|
-
|
|
110
|
-
throw new Error('Server did not bind to a valid local authority');
|
|
143
|
+
await failStartup(new Error('Server did not bind to a valid local authority'));
|
|
111
144
|
}
|
|
112
145
|
|
|
146
|
+
let closing = null;
|
|
147
|
+
const close = () => {
|
|
148
|
+
closing ??= new Promise((resolve, reject) => {
|
|
149
|
+
server.close((error) => error && error.code !== 'ERR_SERVER_NOT_RUNNING'
|
|
150
|
+
? reject(error) : resolve());
|
|
151
|
+
}).then(closeResources);
|
|
152
|
+
return closing;
|
|
153
|
+
};
|
|
113
154
|
server.once('close', () => {
|
|
114
|
-
|
|
115
|
-
|
|
155
|
+
// Legacy callers may close the raw HTTP server; still consume/report asynchronous failures.
|
|
156
|
+
void closeResources().catch((error) => {
|
|
157
|
+
if (!closing) console.error(`Server shutdown failed: ${error.message}`);
|
|
158
|
+
});
|
|
116
159
|
});
|
|
117
160
|
return {
|
|
118
161
|
server,
|
|
162
|
+
close,
|
|
119
163
|
url: `http://${authority}`,
|
|
120
164
|
lanUrls: lanEnabled ? lanConsoleUrls(lanAddresses, address.port) : [],
|
|
121
165
|
app: application
|
|
122
166
|
};
|
|
123
167
|
}
|
|
124
168
|
|
|
125
|
-
|
|
126
|
-
throw new Error('Could not find a fetchable local port');
|
|
169
|
+
await failStartup(new Error('Could not find a fetchable local port'));
|
|
127
170
|
}
|
|
128
171
|
|
|
129
172
|
function resolveServerGraphOptions(options) {
|
|
@@ -178,14 +221,19 @@ async function runProgram(args) {
|
|
|
178
221
|
const runtimeOptions = resolveGraphRuntimeOptions(args, process.env);
|
|
179
222
|
const port = numberOption(args, '--port', DEFAULT_FULI_PORT);
|
|
180
223
|
const lan = args.includes('--lan');
|
|
181
|
-
const { server, url, lanUrls } = await createServer({
|
|
224
|
+
const { server, close: closeRuntime, url, lanUrls } = await createServer({
|
|
182
225
|
...runtimeOptions,
|
|
183
226
|
port,
|
|
184
227
|
lan,
|
|
185
228
|
lanAccessToken: lan ? process.env.FULI_LAN_ACCESS_TOKEN : null
|
|
186
229
|
});
|
|
187
230
|
const handlers = new Map();
|
|
188
|
-
const close = () =>
|
|
231
|
+
const close = () => {
|
|
232
|
+
void closeRuntime().catch((error) => {
|
|
233
|
+
console.error(`Server shutdown failed: ${error.message}`);
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
});
|
|
236
|
+
};
|
|
189
237
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
190
238
|
const handler = close;
|
|
191
239
|
handlers.set(signal, handler);
|
|
@@ -3,6 +3,13 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import { replaceFuliTable } from './codex-config.js';
|
|
4
4
|
import { isCodexBootstrapCurrent } from './codex-bootstrap.js';
|
|
5
5
|
import { hasCurrentClaudeCodeHooks } from './claude-code-config.js';
|
|
6
|
+
import {
|
|
7
|
+
hasCodexTomlHookDefinitions,
|
|
8
|
+
hasCurrentCodexLifecycleHooks,
|
|
9
|
+
hasCurrentCodexTomlLifecycleHooks,
|
|
10
|
+
withCodexTomlLifecycleHooks
|
|
11
|
+
} from '../agents/codex/lifecycle-hooks.js';
|
|
12
|
+
import { hasCurrentCursorLifecycleHooks } from '../agents/cursor/lifecycle-hooks.js';
|
|
6
13
|
import { isSessionSkillCurrent } from './session-skill.js';
|
|
7
14
|
import { readJsonFile } from '../storage/json-file.js';
|
|
8
15
|
|
|
@@ -30,7 +37,19 @@ export function inspectAgentInstallations(agents, context, {
|
|
|
30
37
|
{ fileExists, readText }
|
|
31
38
|
);
|
|
32
39
|
const claudeLifecycleCurrent = agent.id !== 'claude-code' ||
|
|
33
|
-
hasCurrentClaudeCodeHooks(readJson(agent.settingsPath)
|
|
40
|
+
hasCurrentClaudeCodeHooks(readJson(agent.settingsPath), {
|
|
41
|
+
hookTimeoutSec: context.hookTimeoutSec ?? 30
|
|
42
|
+
});
|
|
43
|
+
const codexConfig = agent.id === 'codex' && agent.hooksPath
|
|
44
|
+
? readText(agent.configPath)
|
|
45
|
+
: '';
|
|
46
|
+
const lifecycleCurrent = agent.id === 'codex' && agent.hooksPath
|
|
47
|
+
? hasCodexTomlHookDefinitions(codexConfig)
|
|
48
|
+
? hasCurrentCodexTomlLifecycleHooks(codexConfig, { context })
|
|
49
|
+
: hasCurrentCodexLifecycleHooks(readJson(agent.hooksPath), { context })
|
|
50
|
+
: agent.id === 'cursor' && agent.hooksPath
|
|
51
|
+
? hasCurrentCursorLifecycleHooks(readJson(agent.hooksPath), context)
|
|
52
|
+
: claudeLifecycleCurrent;
|
|
34
53
|
|
|
35
54
|
let integrationStatus = 'not_connected';
|
|
36
55
|
if (
|
|
@@ -38,6 +57,7 @@ export function inspectAgentInstallations(agents, context, {
|
|
|
38
57
|
&& skillsCurrent
|
|
39
58
|
&& codexBootstrapCurrent
|
|
40
59
|
&& claudeLifecycleCurrent
|
|
60
|
+
&& lifecycleCurrent
|
|
41
61
|
) {
|
|
42
62
|
integrationStatus = 'connected';
|
|
43
63
|
}
|
|
@@ -52,8 +72,8 @@ export function inspectAgentInstallations(agents, context, {
|
|
|
52
72
|
...(agent.id === 'codex'
|
|
53
73
|
? { bootstrap: codexBootstrapCurrent ? 'current' : 'outdated' }
|
|
54
74
|
: {}),
|
|
55
|
-
...(agent.id === 'claude-code'
|
|
56
|
-
? { lifecycleHooks:
|
|
75
|
+
...(agent.id === 'claude-code' || agent.hooksPath
|
|
76
|
+
? { lifecycleHooks: lifecycleCurrent ? 'current' : 'outdated' }
|
|
57
77
|
: {})
|
|
58
78
|
}
|
|
59
79
|
};
|
|
@@ -75,9 +95,13 @@ function inspectRegistration(agent, context, readers) {
|
|
|
75
95
|
if (agent.id === 'codex') {
|
|
76
96
|
const current = readers.readText(agent.configPath);
|
|
77
97
|
const present = /^\s*\[mcp_servers\.(?:fuli|"fuli"|'fuli')]/m.test(current);
|
|
98
|
+
const expectedConfig = replaceFuliTable(current, expected);
|
|
99
|
+
const normalizedExpected = hasCodexTomlHookDefinitions(current)
|
|
100
|
+
? withCodexTomlLifecycleHooks(expectedConfig, { context })
|
|
101
|
+
: expectedConfig;
|
|
78
102
|
return {
|
|
79
103
|
present,
|
|
80
|
-
current: present && normalizeText(
|
|
104
|
+
current: present && normalizeText(normalizedExpected) ===
|
|
81
105
|
normalizeText(current)
|
|
82
106
|
};
|
|
83
107
|
}
|
|
@@ -108,9 +132,17 @@ function normalizeText(value) {
|
|
|
108
132
|
}
|
|
109
133
|
|
|
110
134
|
function defaultReadText(path) {
|
|
111
|
-
|
|
135
|
+
try {
|
|
136
|
+
return existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
137
|
+
} catch {
|
|
138
|
+
return '';
|
|
139
|
+
}
|
|
112
140
|
}
|
|
113
141
|
|
|
114
142
|
function defaultReadJson(path) {
|
|
115
|
-
|
|
143
|
+
try {
|
|
144
|
+
return readJsonFile(path, {});
|
|
145
|
+
} catch {
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
116
148
|
}
|
package/src/setup/agents.js
CHANGED
|
@@ -20,6 +20,8 @@ export function discoverAgents({
|
|
|
20
20
|
} = {}) {
|
|
21
21
|
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
22
22
|
const codexHome = nonEmpty(env.CODEX_HOME) ?? pathApi.join(homeDir, '.codex');
|
|
23
|
+
const claudeHome = pathApi.join(homeDir, '.claude');
|
|
24
|
+
const cursorHome = pathApi.join(homeDir, '.cursor');
|
|
23
25
|
const skillName = 'capturing-session-knowledge';
|
|
24
26
|
const projectSkillName = 'grilling-project';
|
|
25
27
|
return [
|
|
@@ -28,6 +30,7 @@ export function discoverAgents({
|
|
|
28
30
|
label: 'Codex',
|
|
29
31
|
command: 'codex',
|
|
30
32
|
configPath: pathApi.join(codexHome, 'config.toml'),
|
|
33
|
+
hooksPath: pathApi.join(codexHome, 'hooks.json'),
|
|
31
34
|
globalInstructionsPath: pathApi.join(codexHome, 'AGENTS.md'),
|
|
32
35
|
globalInstructionsOverridePath: pathApi.join(codexHome, 'AGENTS.override.md'),
|
|
33
36
|
skillPath: pathApi.join(homeDir, '.agents', 'skills', skillName),
|
|
@@ -39,19 +42,28 @@ export function discoverAgents({
|
|
|
39
42
|
label: 'Claude Code',
|
|
40
43
|
command: 'claude',
|
|
41
44
|
configPath: pathApi.join(homeDir, '.claude.json'),
|
|
42
|
-
settingsPath: pathApi.join(
|
|
43
|
-
skillPath: pathApi.join(
|
|
44
|
-
projectSkillPath: pathApi.join(
|
|
45
|
-
available: Boolean(
|
|
45
|
+
settingsPath: pathApi.join(claudeHome, 'settings.json'),
|
|
46
|
+
skillPath: pathApi.join(claudeHome, 'skills', skillName),
|
|
47
|
+
projectSkillPath: pathApi.join(claudeHome, 'skills', projectSkillName),
|
|
48
|
+
available: Boolean(
|
|
49
|
+
commandExists('claude') ||
|
|
50
|
+
fileExists(pathApi.join(homeDir, '.claude.json')) ||
|
|
51
|
+
fileExists(claudeHome)
|
|
52
|
+
)
|
|
46
53
|
},
|
|
47
54
|
{
|
|
48
55
|
id: 'cursor',
|
|
49
56
|
label: 'Cursor',
|
|
50
57
|
command: 'cursor',
|
|
51
|
-
configPath: pathApi.join(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
configPath: pathApi.join(cursorHome, 'mcp.json'),
|
|
59
|
+
hooksPath: pathApi.join(cursorHome, 'hooks.json'),
|
|
60
|
+
skillPath: pathApi.join(cursorHome, 'skills', skillName),
|
|
61
|
+
projectSkillPath: pathApi.join(cursorHome, 'skills', projectSkillName),
|
|
62
|
+
available: Boolean(
|
|
63
|
+
commandExists('cursor') ||
|
|
64
|
+
fileExists(pathApi.join(cursorHome, 'mcp.json')) ||
|
|
65
|
+
fileExists(cursorHome)
|
|
66
|
+
)
|
|
55
67
|
}
|
|
56
68
|
].map((agent) => adaptAgentForReviewSkill(agent, { homeDir, pathApi }));
|
|
57
69
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
readJsonFile,
|
|
@@ -10,6 +11,12 @@ const MANAGED_HOOK_TOOLS = new Set([
|
|
|
10
11
|
'begin_task_context',
|
|
11
12
|
'verify_task_checkpoint'
|
|
12
13
|
]);
|
|
14
|
+
const MANAGED_PERMISSION_RULES = Object.freeze([
|
|
15
|
+
// These allow model-initiated fallback restore/final checkpoint calls. Claude
|
|
16
|
+
// Code executes MCP lifecycle hooks separately from model tool permissions.
|
|
17
|
+
'mcp__fuli__get_collaboration_preferences',
|
|
18
|
+
'mcp__fuli__checkpoint_task_knowledge'
|
|
19
|
+
]);
|
|
13
20
|
|
|
14
21
|
export function connectClaudeCode(agent, context, {
|
|
15
22
|
readConfig = readJsonFile,
|
|
@@ -21,6 +28,7 @@ export function connectClaudeCode(agent, context, {
|
|
|
21
28
|
assertObject(settings, 'Claude Code settings');
|
|
22
29
|
assertObject(current.mcpServers ?? {}, 'Claude Code mcpServers');
|
|
23
30
|
assertHookSettings(settings.hooks ?? {});
|
|
31
|
+
assertPermissionSettings(settings.permissions ?? {});
|
|
24
32
|
|
|
25
33
|
const next = {
|
|
26
34
|
...current,
|
|
@@ -81,7 +89,8 @@ export function disconnectClaudeCode(agent, {
|
|
|
81
89
|
const settings = readConfig(agent.settingsPath, {});
|
|
82
90
|
assertObject(settings, 'Claude Code settings');
|
|
83
91
|
assertHookSettings(settings.hooks ?? {});
|
|
84
|
-
|
|
92
|
+
assertPermissionSettings(settings.permissions ?? {});
|
|
93
|
+
const nextSettings = withoutManagedSettings(settings);
|
|
85
94
|
if (!sameJson(settings, nextSettings)) {
|
|
86
95
|
writeConfig(agent.settingsPath, nextSettings);
|
|
87
96
|
changed = true;
|
|
@@ -95,26 +104,23 @@ export function disconnectClaudeCode(agent, {
|
|
|
95
104
|
};
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
export function hasCurrentClaudeCodeHooks(settings) {
|
|
107
|
+
export function hasCurrentClaudeCodeHooks(settings, { hookTimeoutSec = 30 } = {}) {
|
|
99
108
|
try {
|
|
100
109
|
assertObject(settings, 'Claude Code settings');
|
|
101
110
|
assertHookSettings(settings.hooks ?? {});
|
|
111
|
+
assertPermissionSettings(settings.permissions ?? {});
|
|
112
|
+
return sameJson(
|
|
113
|
+
withManagedHooks(settings, { hookTimeoutSec }),
|
|
114
|
+
settings
|
|
115
|
+
);
|
|
102
116
|
} catch {
|
|
103
117
|
return false;
|
|
104
118
|
}
|
|
105
|
-
return [
|
|
106
|
-
['UserPromptSubmit', 'begin_task_context'],
|
|
107
|
-
['Stop', 'verify_task_checkpoint']
|
|
108
|
-
].every(([event, tool]) => (
|
|
109
|
-
(settings.hooks?.[event] ?? []).some((group) =>
|
|
110
|
-
group.hooks.some((hook) => isManagedHook(hook, tool))
|
|
111
|
-
)
|
|
112
|
-
));
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
function withManagedHooks(settings, { hookTimeoutSec = 30 } = {}) {
|
|
116
122
|
const normalizedHookTimeoutSec = positiveHookTimeout(hookTimeoutSec);
|
|
117
|
-
const clean =
|
|
123
|
+
const clean = withoutManagedSettings(settings);
|
|
118
124
|
const hooks = { ...(clean.hooks ?? {}) };
|
|
119
125
|
hooks.UserPromptSubmit = [
|
|
120
126
|
...(hooks.UserPromptSubmit ?? []),
|
|
@@ -133,7 +139,32 @@ function withManagedHooks(settings, { hookTimeoutSec = 30 } = {}) {
|
|
|
133
139
|
normalizedHookTimeoutSec
|
|
134
140
|
)
|
|
135
141
|
];
|
|
136
|
-
return { ...clean, hooks };
|
|
142
|
+
return withManagedPermissions({ ...clean, hooks });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function withManagedPermissions(settings) {
|
|
146
|
+
const permissions = { ...(settings.permissions ?? {}) };
|
|
147
|
+
permissions.allow = [
|
|
148
|
+
...(permissions.allow ?? []),
|
|
149
|
+
...MANAGED_PERMISSION_RULES
|
|
150
|
+
];
|
|
151
|
+
return { ...settings, permissions };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function withoutManagedSettings(settings) {
|
|
155
|
+
return withoutManagedPermissions(withoutManagedHooks(settings));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function withoutManagedPermissions(settings) {
|
|
159
|
+
if (!settings.permissions) return settings;
|
|
160
|
+
const permissions = { ...settings.permissions };
|
|
161
|
+
const remainingAllow = (permissions.allow ?? [])
|
|
162
|
+
.filter(rule => !MANAGED_PERMISSION_RULES.includes(rule));
|
|
163
|
+
if (remainingAllow.length) permissions.allow = remainingAllow;
|
|
164
|
+
else delete permissions.allow;
|
|
165
|
+
if (Object.keys(permissions).length) return { ...settings, permissions };
|
|
166
|
+
const { permissions: _removed, ...rest } = settings;
|
|
167
|
+
return rest;
|
|
137
168
|
}
|
|
138
169
|
|
|
139
170
|
function withoutManagedHooks(settings) {
|
|
@@ -197,6 +228,17 @@ function assertHookSettings(value) {
|
|
|
197
228
|
}
|
|
198
229
|
}
|
|
199
230
|
|
|
231
|
+
function assertPermissionSettings(value) {
|
|
232
|
+
assertObject(value, 'Claude Code permissions');
|
|
233
|
+
for (const field of ['allow', 'ask', 'deny']) {
|
|
234
|
+
if (value[field] !== undefined && (
|
|
235
|
+
!Array.isArray(value[field]) || !value[field].every(rule => typeof rule === 'string')
|
|
236
|
+
)) {
|
|
237
|
+
throw new TypeError(`Claude Code permissions ${field} must be an array of strings`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
200
242
|
function assertObject(value, label) {
|
|
201
243
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
202
244
|
throw new TypeError(`${label} must be a JSON object`);
|
|
@@ -204,5 +246,5 @@ function assertObject(value, label) {
|
|
|
204
246
|
}
|
|
205
247
|
|
|
206
248
|
function sameJson(left, right) {
|
|
207
|
-
return
|
|
249
|
+
return isDeepStrictEqual(left, right);
|
|
208
250
|
}
|
|
@@ -15,17 +15,18 @@ const BLOCK_START = '<!-- BEGIN FULI MANAGED INSTRUCTIONS -->';
|
|
|
15
15
|
const BLOCK_END = '<!-- END FULI MANAGED INSTRUCTIONS -->';
|
|
16
16
|
|
|
17
17
|
const GLOBAL_INSTRUCTIONS = `${BLOCK_START}
|
|
18
|
-
At start of every user task,
|
|
18
|
+
At start of every user task, use \`begin_task_context\` hook context if supplied;
|
|
19
|
+
do not repeat the fallback. Otherwise before any other tool or answer, call exactly
|
|
19
20
|
\`get_collaboration_preferences\` with \`projectPath\`=current working directory,
|
|
20
|
-
\`taskPrompt\`=current user request. Never substitute a project action
|
|
21
|
-
Fuli never stores or returns them
|
|
22
|
-
Before
|
|
23
|
-
On miss
|
|
24
|
-
|
|
25
|
-
Write tools
|
|
26
|
-
Resolve relevant \`deferred_conflict
|
|
27
|
-
Team: \`coordinate_project_agent_task\`;
|
|
28
|
-
|
|
21
|
+
\`taskPrompt\`=current user request. Never substitute a project action.
|
|
22
|
+
Fuli never stores or returns them. Apply all returned \`effective_preferences\`.
|
|
23
|
+
Before reasking a stable project fact or method, inspect \`task_knowledge_recall\`.
|
|
24
|
+
On miss: \`search_current_project_knowledge\`; focused action/artifact/target/identifier;
|
|
25
|
+
never use the full request as the only query.
|
|
26
|
+
Write tools: preferences in actual payload; mentioning later is not compliance.
|
|
27
|
+
Resolve relevant \`deferred_conflict\`; ignore others.
|
|
28
|
+
Team: \`coordinate_project_agent_task\`; \`acquire_runtime_lease\`, run/report workers,
|
|
29
|
+
\`release_runtime_lease\` in finally.
|
|
29
30
|
${codexReviewCommandBridge()}
|
|
30
31
|
${BLOCK_END}`;
|
|
31
32
|
|
|
@@ -13,14 +13,27 @@ import {
|
|
|
13
13
|
installCodexBootstrap,
|
|
14
14
|
removeCodexBootstrap
|
|
15
15
|
} from './codex-bootstrap.js';
|
|
16
|
+
import {
|
|
17
|
+
hasCodexTomlHookDefinitions,
|
|
18
|
+
hasCurrentCodexTomlLifecycleHooks,
|
|
19
|
+
installCodexLifecycleHooks,
|
|
20
|
+
removeCodexLifecycleHooks,
|
|
21
|
+
withCodexTomlLifecycleHooks,
|
|
22
|
+
withoutCodexLifecycleHooks,
|
|
23
|
+
withoutCodexTomlLifecycleHooks
|
|
24
|
+
} from '../agents/codex/lifecycle-hooks.js';
|
|
25
|
+
import { readJsonFile, writeJsonFileAtomic } from '../storage/json-file.js';
|
|
16
26
|
|
|
17
27
|
export function connectCodex(agent, context, {
|
|
18
28
|
readConfig = readText,
|
|
19
29
|
writeConfig = writeTextAtomic,
|
|
20
|
-
|
|
30
|
+
readHooks = readJsonFile,
|
|
31
|
+
writeHooks = writeJsonFileAtomic,
|
|
32
|
+
installBootstrap = installCodexBootstrap,
|
|
33
|
+
installHooks = installCodexLifecycleHooks
|
|
21
34
|
} = {}) {
|
|
22
35
|
const current = readConfig(agent.configPath);
|
|
23
|
-
|
|
36
|
+
let next = replaceFuliTable(current, {
|
|
24
37
|
command: context.nodePath,
|
|
25
38
|
args: [
|
|
26
39
|
context.mcpServerPath,
|
|
@@ -30,28 +43,75 @@ export function connectCodex(agent, context, {
|
|
|
30
43
|
'codex'
|
|
31
44
|
]
|
|
32
45
|
});
|
|
46
|
+
const tomlHooksPresent = Boolean(
|
|
47
|
+
agent.hooksPath && hasCodexTomlHookDefinitions(current)
|
|
48
|
+
);
|
|
49
|
+
const currentHooks = agent.hooksPath ? readHooks(agent.hooksPath, {}) : null;
|
|
50
|
+
const remainingHooks = currentHooks === null
|
|
51
|
+
? null
|
|
52
|
+
: withoutCodexLifecycleHooks(currentHooks);
|
|
53
|
+
if (tomlHooksPresent && hasHookDefinitions(remainingHooks)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'Codex has unrelated hooks in both config.toml and hooks.json; ' +
|
|
56
|
+
'Fuli cannot safely choose one representation automatically.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
33
60
|
const bootstrap = installBootstrap(agent, context);
|
|
61
|
+
let hooks;
|
|
62
|
+
let hooksJsonWrite = null;
|
|
63
|
+
if (tomlHooksPresent) {
|
|
64
|
+
const withHooks = withCodexTomlLifecycleHooks(next, { context });
|
|
65
|
+
const jsonChanged = JSON.stringify(currentHooks) !== JSON.stringify(remainingHooks);
|
|
66
|
+
const tomlHooksChanged = !hasCurrentCodexTomlLifecycleHooks(current, {
|
|
67
|
+
context
|
|
68
|
+
});
|
|
69
|
+
hooks = {
|
|
70
|
+
changed: tomlHooksChanged || jsonChanged,
|
|
71
|
+
trustReviewRequired: tomlHooksChanged || jsonChanged
|
|
72
|
+
};
|
|
73
|
+
next = withHooks;
|
|
74
|
+
if (jsonChanged) hooksJsonWrite = remainingHooks;
|
|
75
|
+
} else {
|
|
76
|
+
hooks = installHooks(agent, { context });
|
|
77
|
+
}
|
|
34
78
|
const registrationChanged = next !== current;
|
|
35
79
|
if (registrationChanged) writeConfig(agent.configPath, next);
|
|
80
|
+
if (hooksJsonWrite !== null) writeHooks(agent.hooksPath, hooksJsonWrite);
|
|
36
81
|
return {
|
|
37
82
|
id: agent.id,
|
|
38
83
|
label: agent.label,
|
|
39
84
|
status: 'connected',
|
|
40
|
-
newTaskRequired: registrationChanged || bootstrap.changed === true
|
|
85
|
+
newTaskRequired: registrationChanged || bootstrap.changed === true || hooks.changed === true,
|
|
86
|
+
...(hooks.trustReviewRequired === true ? { trustReviewRequired: true } : {}),
|
|
87
|
+
...(agent.hooksPath ? {
|
|
88
|
+
nextSteps: [hooks.trustReviewRequired === true
|
|
89
|
+
? 'Review and trust Fuli hooks in the Codex CLI with /hooks ' +
|
|
90
|
+
'before relying on automatic task entry.'
|
|
91
|
+
: 'Check Fuli hooks in the Codex CLI with /hooks; setup does not verify hook trust.']
|
|
92
|
+
} : {})
|
|
41
93
|
};
|
|
42
94
|
}
|
|
43
95
|
|
|
96
|
+
function hasHookDefinitions(value) {
|
|
97
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') return false;
|
|
98
|
+
if (!value.hooks || Array.isArray(value.hooks) || typeof value.hooks !== 'object') return false;
|
|
99
|
+
return Object.values(value.hooks).some((groups) => Array.isArray(groups) && groups.length > 0);
|
|
100
|
+
}
|
|
101
|
+
|
|
44
102
|
export function disconnectCodex(agent, {
|
|
45
103
|
fileExists = existsSync,
|
|
46
104
|
readConfig = readText,
|
|
47
105
|
writeConfig = writeTextAtomic,
|
|
48
|
-
removeBootstrap = removeCodexBootstrap
|
|
106
|
+
removeBootstrap = removeCodexBootstrap,
|
|
107
|
+
removeHooks = removeCodexLifecycleHooks
|
|
49
108
|
} = {}) {
|
|
50
109
|
const bootstrap = removeBootstrap(agent);
|
|
110
|
+
const hooks = removeHooks(agent);
|
|
51
111
|
let registrationChanged = false;
|
|
52
112
|
if (fileExists(agent.configPath)) {
|
|
53
113
|
const current = readConfig(agent.configPath);
|
|
54
|
-
const next = removeFuliTables(current);
|
|
114
|
+
const next = removeFuliTables(withoutCodexTomlLifecycleHooks(current));
|
|
55
115
|
if (next !== current) {
|
|
56
116
|
writeConfig(agent.configPath, next);
|
|
57
117
|
registrationChanged = true;
|
|
@@ -60,7 +120,7 @@ export function disconnectCodex(agent, {
|
|
|
60
120
|
return {
|
|
61
121
|
id: agent.id,
|
|
62
122
|
label: agent.label,
|
|
63
|
-
status: registrationChanged || bootstrap.changed ? 'disconnected' : 'not_connected'
|
|
123
|
+
status: registrationChanged || bootstrap.changed || hooks.changed ? 'disconnected' : 'not_connected'
|
|
64
124
|
};
|
|
65
125
|
}
|
|
66
126
|
|
|
@@ -88,7 +148,7 @@ export function removeFuliTables(source) {
|
|
|
88
148
|
let removed = false;
|
|
89
149
|
|
|
90
150
|
for (const line of lines) {
|
|
91
|
-
const header =
|
|
151
|
+
const header = tomlHeaderName(line);
|
|
92
152
|
if (header) {
|
|
93
153
|
skipping = isFuliTable(header);
|
|
94
154
|
if (skipping) removed = true;
|
|
@@ -101,6 +161,13 @@ export function removeFuliTables(source) {
|
|
|
101
161
|
return kept.length ? `${kept.join('\n')}\n` : '';
|
|
102
162
|
}
|
|
103
163
|
|
|
164
|
+
function tomlHeaderName(line) {
|
|
165
|
+
return (
|
|
166
|
+
/^\s*\[\[([^\]]+)]]\s*(?:#.*)?$/.exec(line)?.[1] ??
|
|
167
|
+
/^\s*\[([^\]]+)]\s*(?:#.*)?$/.exec(line)?.[1]
|
|
168
|
+
)?.trim();
|
|
169
|
+
}
|
|
170
|
+
|
|
104
171
|
function isFuliTable(header) {
|
|
105
172
|
return /^mcp_servers\.(?:fuli|"fuli"|'fuli')(?:\.|$)/.test(header);
|
|
106
173
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
chmodSync,
|
|
2
3
|
copyFileSync,
|
|
3
4
|
existsSync,
|
|
4
5
|
mkdirSync
|
|
@@ -10,16 +11,30 @@ export function backupAgentConfig(agent, {
|
|
|
10
11
|
now = () => new Date(),
|
|
11
12
|
fileExists = existsSync,
|
|
12
13
|
makeDirectory = mkdirSync,
|
|
13
|
-
copyFile = copyFileSync
|
|
14
|
+
copyFile = copyFileSync,
|
|
15
|
+
setMode = chmodSync
|
|
14
16
|
}) {
|
|
15
17
|
const sources = [
|
|
16
18
|
{ path: agent.configPath, suffix: '' },
|
|
17
19
|
...(agent.settingsPath
|
|
18
20
|
? [{ path: agent.settingsPath, suffix: '-settings' }]
|
|
21
|
+
: []),
|
|
22
|
+
...(agent.hooksPath
|
|
23
|
+
? [{ path: agent.hooksPath, suffix: '-hooks' }]
|
|
24
|
+
: []),
|
|
25
|
+
...(agent.globalInstructionsPath
|
|
26
|
+
? [{ path: agent.globalInstructionsPath, suffix: '-instructions' }]
|
|
27
|
+
: []),
|
|
28
|
+
...(agent.globalInstructionsOverridePath
|
|
29
|
+
? [{
|
|
30
|
+
path: agent.globalInstructionsOverridePath,
|
|
31
|
+
suffix: '-instructions-override'
|
|
32
|
+
}]
|
|
19
33
|
: [])
|
|
20
34
|
].filter(({ path }) => path && fileExists(path));
|
|
21
35
|
if (sources.length === 0) return null;
|
|
22
|
-
makeDirectory(backupDir, { recursive: true });
|
|
36
|
+
makeDirectory(backupDir, { recursive: true, mode: 0o700 });
|
|
37
|
+
setMode(backupDir, 0o700);
|
|
23
38
|
const timestamp = now().toISOString().replaceAll(':', '-').replaceAll('.', '-');
|
|
24
39
|
const backups = sources.map(({ path, suffix }) => {
|
|
25
40
|
const extension = extname(path) || '.bak';
|
|
@@ -28,6 +43,7 @@ export function backupAgentConfig(agent, {
|
|
|
28
43
|
`${agent.id}${suffix}-${timestamp}${extension}`
|
|
29
44
|
);
|
|
30
45
|
copyFile(path, backupPath);
|
|
46
|
+
setMode(backupPath, 0o600);
|
|
31
47
|
return { path, backupPath };
|
|
32
48
|
});
|
|
33
49
|
return (
|