fuli-context 0.10.0 → 0.11.1

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 (212) hide show
  1. package/README.md +27 -30
  2. package/README.zh-CN.md +22 -26
  3. package/dist/web/assets/AboutPage-cW9hDghN.js +1 -0
  4. package/dist/web/assets/AgentConversations-CGznTWuV.js +2 -0
  5. package/dist/web/assets/AgentConversations-XqNEBY_2.css +1 -0
  6. package/dist/web/assets/AgentHand-DkyF4vgk.js +1 -0
  7. package/dist/web/assets/AgentName-C1mAVr-v.js +1 -0
  8. package/dist/web/assets/AgentProfilePage-pKUR0rfj.js +1 -0
  9. package/dist/web/assets/AgentsDirectoryPage-CUlbT2hD.js +1 -0
  10. package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +1 -0
  11. package/dist/web/assets/ConnectionsPage-CM9C-zEI.css +1 -0
  12. package/dist/web/assets/ConnectionsPage-CuDHIJXu.js +1 -0
  13. package/dist/web/assets/EmployeeRecruitDialog-CVbqCzHb.css +1 -0
  14. package/dist/web/assets/EmployeeRecruitDialog-D9QfGipd.js +1 -0
  15. package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +1 -0
  16. package/dist/web/assets/EmployeeWorkbenchPage-DAtvGney.js +3 -0
  17. package/dist/web/assets/KnowledgeEditDialog-a-5rasG0.js +1 -0
  18. package/dist/web/assets/KnowledgeEditDialog-hT9znkoe.css +1 -0
  19. package/dist/web/assets/KnowledgeInspector-BGsqjNY2.js +1 -0
  20. package/dist/web/assets/KnowledgeOrganizerPage-BndgUsHS.js +1 -0
  21. package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +1 -0
  22. package/dist/web/assets/KnowledgePage-CQweO0YJ.js +1 -0
  23. package/dist/web/assets/KnowledgeWorkspace-BtUjKTLb.js +4 -0
  24. package/dist/web/assets/KnowledgeWorkspace-D4a7FcAb.css +1 -0
  25. package/dist/web/assets/NotFoundPage-BmQTTqQn.js +1 -0
  26. package/dist/web/assets/OverviewPage-By-fUzSG.css +1 -0
  27. package/dist/web/assets/OverviewPage-C73HyTsM.js +1 -0
  28. package/dist/web/assets/PersonalProfilePage-BMuOrppx.js +1 -0
  29. package/dist/web/assets/PersonalProfilePage-DrMDAAdy.css +1 -0
  30. package/dist/web/assets/PersonalProjectsPage-Cq9_kFvs.js +1 -0
  31. package/dist/web/assets/ProjectAgentFirstTask-Bi1ZsTC1.css +1 -0
  32. package/dist/web/assets/ProjectAgentFirstTask-CaHvOjkp.js +1 -0
  33. package/dist/web/assets/ProjectAgentsPage-DWGkgIsr.css +1 -0
  34. package/dist/web/assets/ProjectAgentsPage-WVcNdzVi.js +12 -0
  35. package/dist/web/assets/ProjectScopePicker-zAgiaZKL.js +1 -0
  36. package/dist/web/assets/PublicProjectsPage-DKamXDV9.css +1 -0
  37. package/dist/web/assets/PublicProjectsPage-DL093f65.js +1 -0
  38. package/dist/web/assets/ReviewPage-D_NIhPx8.js +1 -0
  39. package/dist/web/assets/RoundtablesPage-Cb0R4GaM.css +1 -0
  40. package/dist/web/assets/RoundtablesPage-CdVVyZE-.js +1 -0
  41. package/dist/web/assets/SettingsPage-BmYkLul4.css +1 -0
  42. package/dist/web/assets/SettingsPage-CbLaau8Q.js +1 -0
  43. package/dist/web/assets/SharedJefaReportPage-aygd6wvb.js +1 -0
  44. package/dist/web/assets/UiDialog-Q7fsJUoO.js +1 -0
  45. package/dist/web/assets/UiDisclosure-BS-muK5j.js +1 -0
  46. package/dist/web/assets/UiSegmented-CT28bxqw.js +1 -0
  47. package/dist/web/assets/UiSelect-Bv6R0yr1.css +1 -0
  48. package/dist/web/assets/UiSelect-DcGpPQQo.js +1 -0
  49. package/dist/web/assets/WritingTastePage-B_zYD1df.css +1 -0
  50. package/dist/web/assets/WritingTastePage-CYlwAFqV.js +1 -0
  51. package/dist/web/assets/agent-profile-BZj3ezWe.css +1 -0
  52. package/dist/web/assets/{attention-store-Dk-_P4Oi.js → attention-store-D-qIFRIb.js} +1 -1
  53. package/dist/web/assets/{catalog-DN72452f.js → catalog-UDv854JN.js} +1 -1
  54. package/dist/web/assets/client-DLS5FZBI.js +1 -0
  55. package/dist/web/assets/{console-BtPqo79j.js → console-CqzwSo7a.js} +1 -1
  56. package/dist/web/assets/{conversation-launcher-settings-jkHRmWai.js → conversation-launcher-settings-DRoIz42V.js} +1 -1
  57. package/dist/web/assets/{i18n-D2CJkEYK.js → i18n-b14l2pQA.js} +3 -3
  58. package/dist/web/assets/{identity-B3zEw-1u.js → identity-BWbPmZZf.js} +1 -1
  59. package/dist/web/assets/index-D1OHIz4c.js +5 -0
  60. package/dist/web/assets/index-kPB0Cf8Q.css +1 -0
  61. package/dist/web/assets/{model-S7ehAjky.js → model-DupZy8Qb.js} +1 -1
  62. package/dist/web/assets/{personal-profile-graph--GG3Mizo.js → personal-profile-graph-BPTtMJTZ.js} +1 -1
  63. package/dist/web/assets/shared-report-BYnLhmCZ.js +1 -0
  64. package/dist/web/assets/{useAgentRoster-BAPKWZ9y.js → useAgentRoster-ep7SpKv1.js} +1 -1
  65. package/dist/web/assets/{useApi-CROJJdhE-BtFMXFjw.js → useApi-CROJJdhE-CEQAEuug.js} +1 -1
  66. package/dist/web/assets/{vue-router-DkaTtPn0.js → vue-router-B6ZjK-Br.js} +1 -1
  67. package/dist/web/index.html +14 -16
  68. package/docs/agent-conversations-and-collaboration.md +9 -9
  69. package/docs/agent-roundtable.md +77 -0
  70. package/docs/employee-agents.md +2 -2
  71. package/docs/project-agent-memory.md +1 -1
  72. package/graph-provider/fuli_graph/agent_conversation_models.py +11 -2
  73. package/graph-provider/fuli_graph/conversation_context.py +40 -9
  74. package/graph-provider/fuli_graph/default_project_lead.py +88 -0
  75. package/graph-provider/fuli_graph/project_agent_context_models.py +5 -1
  76. package/graph-provider/fuli_graph/project_agent_delegations.py +118 -0
  77. package/graph-provider/fuli_graph/project_agent_employee_numbers.py +41 -0
  78. package/graph-provider/fuli_graph/project_agent_memory_routes.py +20 -0
  79. package/graph-provider/fuli_graph/project_agent_models.py +1 -0
  80. package/graph-provider/fuli_graph/project_agent_task_models.py +3 -0
  81. package/graph-provider/fuli_graph/project_agent_team_growth.py +2 -2
  82. package/graph-provider/fuli_graph/runtime.py +10 -76
  83. package/graph-provider/fuli_graph/store.py +2 -0
  84. package/graph-provider/fuli_graph/store_agent_conversations.py +60 -8
  85. package/graph-provider/fuli_graph/store_project_agent_context.py +52 -4
  86. package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +16 -1
  87. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +3 -1
  88. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +3 -2
  89. package/graph-provider/fuli_graph/store_project_agent_tasks.py +12 -8
  90. package/graph-provider/fuli_graph/store_project_agents.py +56 -129
  91. package/graph-provider/fuli_graph/store_task_contexts.py +26 -0
  92. package/graph-provider/fuli_graph/system_agents.py +130 -0
  93. package/graph-provider/fuli_graph/system_hr_identity.py +1 -1
  94. package/graph-provider/fuli_graph/task_context_routes.py +10 -0
  95. package/graph-provider/pyproject.toml +1 -1
  96. package/npm-shrinkwrap.json +2 -2
  97. package/package.json +2 -2
  98. package/src/agent-roundtable/http.js +14 -0
  99. package/src/agent-roundtable/service.js +247 -0
  100. package/src/agent-roundtable/store.js +86 -0
  101. package/src/agent-roundtable/tools.js +61 -0
  102. package/src/agent-roundtable/wake.js +100 -0
  103. package/src/agent-tools/agent-conversation-definitions.js +3 -3
  104. package/src/agent-tools/graph-definitions.js +3 -2
  105. package/src/agent-tools/interface-catalog.js +0 -5
  106. package/src/agent-tools.js +2 -2
  107. package/src/agents/claude-code/wake.js +37 -0
  108. package/src/agents/codex/wake.js +47 -0
  109. package/src/agents/identity-receipt.js +35 -7
  110. package/src/agents/session-log.js +34 -0
  111. package/src/agents/wake-registry.js +14 -0
  112. package/src/cli/command-registry.js +0 -2
  113. package/src/cli/setup-command.js +2 -5
  114. package/src/cli/update-command.js +13 -2
  115. package/src/cli.js +0 -5
  116. package/src/employees/management-policy.js +4 -2
  117. package/src/employees/manifest.js +3 -0
  118. package/src/employees/recruitment.js +3 -2
  119. package/src/graphiti/agent-conversations.js +11 -9
  120. package/src/graphiti/agent-knowledge-workflows.js +34 -7
  121. package/src/graphiti/federated-application.js +17 -5
  122. package/src/graphiti/project-agent-mapping.js +2 -0
  123. package/src/graphiti/project-agent-task-entry.js +38 -8
  124. package/src/graphiti/project-agent-team-context.js +4 -1
  125. package/src/graphiti/project-path-context.js +7 -2
  126. package/src/graphiti/provider-client.js +17 -0
  127. package/src/graphiti/repository-project-registration.js +17 -0
  128. package/src/graphiti/task-entry-preferences.js +17 -4
  129. package/src/graphiti/task-entry-project-scope.js +67 -0
  130. package/src/http/api-router.js +5 -3
  131. package/src/http/error-mapping.js +0 -2
  132. package/src/mcp/instructions.js +5 -5
  133. package/src/mcp/session-id.js +1 -1
  134. package/src/mcp/tool-annotations.js +3 -4
  135. package/src/setup/agents.js +1 -41
  136. package/src/setup/graph-runtime.js +15 -2
  137. package/src/setup/session-skill.js +22 -2
  138. package/dist/web/assets/AboutPage-D-GyJNv1.js +0 -1
  139. package/dist/web/assets/AgentConversations-DOuZgUeA.js +0 -2
  140. package/dist/web/assets/AgentConversations-DS8h3yKn.css +0 -1
  141. package/dist/web/assets/AgentHand-eGsXoGSq.js +0 -1
  142. package/dist/web/assets/AgentName-Dtmu9Ff5.js +0 -1
  143. package/dist/web/assets/AgentProfilePage-D8Tvayn7.js +0 -1
  144. package/dist/web/assets/AgentsDirectoryPage-BTEfKZjF.js +0 -1
  145. package/dist/web/assets/ConnectionsPage-C1w99yN3.css +0 -1
  146. package/dist/web/assets/ConnectionsPage-DwOIcvl9.js +0 -1
  147. package/dist/web/assets/EmployeeRecruitDialog-BrIMRblq.css +0 -1
  148. package/dist/web/assets/EmployeeRecruitDialog-D11blfwU.js +0 -1
  149. package/dist/web/assets/EmployeeWorkbenchPage-BcEzYt65.css +0 -1
  150. package/dist/web/assets/EmployeeWorkbenchPage-CK2BAoGt.js +0 -3
  151. package/dist/web/assets/GrowthLoading-KTjE2e50.js +0 -1
  152. package/dist/web/assets/KnowledgeEditDialog-B3x8vs3c.css +0 -1
  153. package/dist/web/assets/KnowledgeEditDialog-n9trHQ5d.js +0 -1
  154. package/dist/web/assets/KnowledgeInspector-BcNjgw9H.js +0 -1
  155. package/dist/web/assets/KnowledgeOrganizerPage-BzgxP-nG.css +0 -1
  156. package/dist/web/assets/KnowledgeOrganizerPage-DMBpGxLr.js +0 -1
  157. package/dist/web/assets/KnowledgePage-POkCMW8f.js +0 -1
  158. package/dist/web/assets/KnowledgeWorkspace-2FvQadhB.js +0 -4
  159. package/dist/web/assets/KnowledgeWorkspace-sb8OJ97I.css +0 -1
  160. package/dist/web/assets/NotFoundPage-BCMZbeB8.js +0 -1
  161. package/dist/web/assets/OverviewPage-0CFWCG9-.css +0 -1
  162. package/dist/web/assets/OverviewPage-0mYifpjp.js +0 -1
  163. package/dist/web/assets/PersonalProfilePage-Chv2a2pi.css +0 -1
  164. package/dist/web/assets/PersonalProfilePage-JW6Muix6.js +0 -1
  165. package/dist/web/assets/PersonalProjectsPage-CRbi64JP.js +0 -1
  166. package/dist/web/assets/ProjectAgentFirstTask-6afJ8HUe.css +0 -1
  167. package/dist/web/assets/ProjectAgentFirstTask-DJ0wTbh8.js +0 -1
  168. package/dist/web/assets/ProjectAgentsPage-CPgKVLgg.js +0 -12
  169. package/dist/web/assets/ProjectAgentsPage-DeuRkMp1.css +0 -1
  170. package/dist/web/assets/ProjectScopePicker-BwB7gK-7.js +0 -1
  171. package/dist/web/assets/PublicProjectsPage-CB272Sq-.js +0 -1
  172. package/dist/web/assets/ReviewPage-CelTZU4C.js +0 -1
  173. package/dist/web/assets/RoundtablesPage-C9dXfWCl.js +0 -2
  174. package/dist/web/assets/RoundtablesPage-DCbIBnBq.css +0 -1
  175. package/dist/web/assets/SearchableSelect-f7EHbyDL.js +0 -1
  176. package/dist/web/assets/SettingsPage-5g7mRN2F.css +0 -1
  177. package/dist/web/assets/SettingsPage-nAijmm5f.js +0 -1
  178. package/dist/web/assets/SharedJefaReportPage-CjpJ9bkk.js +0 -1
  179. package/dist/web/assets/UiDisclosure-CWlC91Zt.js +0 -1
  180. package/dist/web/assets/WritingTastePage-DaHWY61P.js +0 -1
  181. package/dist/web/assets/WritingTastePage-SMGF39mf.css +0 -1
  182. package/dist/web/assets/agent-profile-CBvJHGjt.css +0 -1
  183. package/dist/web/assets/client-B2cRl-7c.js +0 -1
  184. package/dist/web/assets/copy-D2LqwtPZ.js +0 -1
  185. package/dist/web/assets/index-CNp0tRNp.css +0 -1
  186. package/dist/web/assets/index-ulISYH02.js +0 -5
  187. package/dist/web/assets/shared-report-DEK9wvhl.js +0 -1
  188. package/dist/web/assets/useModalDialog-D8tiJfbc.js +0 -1
  189. package/docs/roundtable.md +0 -46
  190. package/src/agents/a2a/roundtable-participant.js +0 -374
  191. package/src/agents/claude-code/roundtable-participant.js +0 -24
  192. package/src/agents/codex/roundtable-config.js +0 -32
  193. package/src/agents/codex/roundtable-participant.js +0 -34
  194. package/src/agents/grok/roundtable-participant.js +0 -185
  195. package/src/agents/pi/roundtable-participant.js +0 -193
  196. package/src/agents/pi/roundtable-result-format.js +0 -183
  197. package/src/cli/roundtable-command.js +0 -79
  198. package/src/roundtables/application.js +0 -69
  199. package/src/roundtables/domain.js +0 -181
  200. package/src/roundtables/http-router.js +0 -58
  201. package/src/roundtables/mcp-server.js +0 -55
  202. package/src/roundtables/participant-registry.js +0 -12
  203. package/src/roundtables/process.js +0 -70
  204. package/src/roundtables/profiles.js +0 -84
  205. package/src/roundtables/result-contract.js +0 -47
  206. package/src/roundtables/server.js +0 -52
  207. package/src/roundtables/service.js +0 -480
  208. package/src/roundtables/store.js +0 -63
  209. package/src/roundtables/tool-contract.js +0 -58
  210. package/src/roundtables/turn-prompt.js +0 -24
  211. package/src/roundtables/worker.js +0 -85
  212. /package/dist/web/assets/{GrowthLoading-CIuQ1icy.css → client-CIuQ1icy.css} +0 -0
@@ -1 +0,0 @@
1
- import{A as e,R as t,X as n,k as r}from"./i18n-D2CJkEYK.js";var i=[`button:not([disabled])`,`input:not([disabled]):not([type="hidden"])`,`select:not([disabled])`,`textarea:not([disabled])`,`a[href]`,`[contenteditable="true"]`,`[tabindex]:not([tabindex="-1"])`].join(`,`);function a(i,a){let s=n(null),c=n(null),l=null,u=0;t(i,e=>{let t=++u;if(e){l=document.activeElement instanceof HTMLElement?document.activeElement:null,r().then(()=>{if(t!==u||!i())return;let e=s.value;e&&(e.open||e.showModal(),(c.value??o(e)[0])?.focus({preventScroll:!0}))});return}let n=s.value;n?.open&&n.close(),d(t)},{immediate:!0}),e(()=>{u+=1;let e=s.value;e?.open&&e.close();let t=l;l=null,t?.isConnected&&t.focus({preventScroll:!0})});function d(e){let t=l;l=null,r().then(()=>{e!==u||!t?.isConnected||t.focus({preventScroll:!0})})}function f(e){e.preventDefault(),a()}function p(e){if(e.key!==`Tab`)return;let t=s.value;if(!t)return;let n=o(t);if(!n.length){e.preventDefault(),t.focus({preventScroll:!0});return}let r=n[0],i=n.at(-1)??r,a=document.activeElement;e.shiftKey&&(a===r||!t.contains(a))?(e.preventDefault(),i.focus({preventScroll:!0})):!e.shiftKey&&(a===i||!t.contains(a))&&(e.preventDefault(),r.focus({preventScroll:!0}))}return{dialogRef:s,initialFocusRef:c,onCancel:f,onKeydown:p}}function o(e){return[...e.querySelectorAll(i)].filter(e=>!e.hidden&&e.getAttribute(`aria-hidden`)!==`true`)}export{a as t};
@@ -1,46 +0,0 @@
1
- # Agent Roundtable
2
-
3
- Roundtable gives invited AI participants a shared goal, public responsibilities, addressed messages, and an ordered workflow from discussion to implementation and review.
4
-
5
- ## Start
6
-
7
- Requires Node.js 24.12 or newer.
8
-
9
- ```sh
10
- npm install -g fuli-context@latest
11
- fl roundtable serve --data-dir ./roundtable-data --port 3738
12
- ```
13
-
14
- Open the local Roundtable page, create a goal, select 2–6 seats and issue an invitation for each participant. Each room has one moderator; collaboration rooms also have an implementer and an independent reviewer. Room history persists in the selected data directory.
15
-
16
- ## AI onboarding
17
-
18
- Connect the invited client to the room MCP URL shown by the invitation dialog, with its seat credential in the client's Bearer authentication setting. The MCP server supplies onboarding instructions and tools:
19
-
20
- 1. `discover_roundtable` identifies the participant's seat, workflow, roster and next action.
21
- 2. `join_roundtable` accepts the participant's own introduction, responsibilities and capabilities.
22
- 3. `discover_roundtable` finds matching peers; `message_roundtable` sends a room-visible question or handoff to a selected seat.
23
- 4. `read_roundtable` retrieves shared messages and tasks.
24
- 5. `claim_roundtable_turn` claims the participant's assigned turn. `submit_roundtable_turn` records the result, artifacts and review verdict.
25
-
26
- The room-specific MCP connection supplies its room ID automatically. When using these tools through the general FULI MCP server, supply the room ID and seat credential explicitly.
27
-
28
- Self-descriptions help peers choose collaborators. They do not change the seat's execution grants. Peer messages are shared task material; room ownership and permissions remain with the user.
29
-
30
- ## Local workers
31
-
32
- Store the invitation in `FULI_ROUNDTABLE_TOKEN`, then run a worker with an already configured client:
33
-
34
- ```sh
35
- fl roundtable worker --url https://COORDINATOR --room ROOM_ID --runtime codex --workspace ./project
36
- ```
37
-
38
- Use `claude-code`, `pi`, `grok` or `a2a` for the corresponding adapter. Workers default to read only. Implementation requires both an owner-granted workspace and `--allow-write`. Reviews remain read only.
39
-
40
- Grok uses `XAI_API_KEY` and `XAI_MODEL`. A2A uses `--a2a-url` and, when needed, `FULI_A2A_TOKEN`. Pi uses an installed Ollama model selected with `--model`; it is an experimental adapter and requires a tool-capable model with sufficient context.
41
-
42
- ## Sharing a coordinator
43
-
44
- Use a reachable HTTPS endpoint and `--public-url https://COORDINATOR`. Expose `/roundtable-peer/` through the proxy; retain the owner console on local loopback. Each invitation is limited to one room and seat and can be revoked.
45
-
46
- Roundtable shares task messages and artifact references. Give reviewers authorized access to the actual artifacts through the shared workspace or referenced revision. Pause, resume, stop and completion remain available to the room owner.
@@ -1,374 +0,0 @@
1
- import { lookup } from 'node:dns/promises';
2
- import { isIP } from 'node:net';
3
- import { randomUUID } from 'node:crypto';
4
- import { setTimeout as delay } from 'node:timers/promises';
5
- import { request as httpsRequest } from 'node:https';
6
- import { Readable } from 'node:stream';
7
-
8
- const OUTPUT_LIMIT = 16 * 1024;
9
- const RESPONSE_LIMIT = 1024 * 1024;
10
-
11
- class A2AParticipantError extends Error {
12
- constructor(code, status = null) {
13
- const messages = {
14
- invalid_endpoint: 'A2A requires an explicitly configured public HTTPS endpoint on one origin.',
15
- waiting_auth: 'Remote A2A authentication or permission is required.',
16
- waiting_input: 'The remote A2A participant requires additional input.',
17
- unsupported_protocol: 'The remote agent does not advertise a supported A2A protocol interface.',
18
- response_invalid: 'The remote A2A participant returned an invalid or mismatched result.',
19
- response_too_large: 'The remote A2A response exceeds the transport limit.',
20
- output_too_large: 'The remote A2A output exceeds the roundtable message limit.',
21
- remote_failed: 'The remote A2A task or request failed.',
22
- cancelled: 'The remote A2A turn was interrupted.',
23
- timeout: 'The remote A2A turn timed out.',
24
- participant_busy: 'The A2A participant already has a running turn.',
25
- participant_closed: 'The A2A participant is closed.'
26
- };
27
- super(messages[code] ?? messages.remote_failed);
28
- this.code = code;
29
- if (status !== null) this.status = status;
30
- }
31
- }
32
-
33
- function publicAddress(address) {
34
- if (isIP(address) === 4) {
35
- const [a, b] = address.split('.').map(Number);
36
- return a !== 0 && a !== 10 && a !== 127 && a < 224 &&
37
- !(a === 169 && b === 254) && !(a === 172 && b >= 16 && b <= 31) &&
38
- !(a === 192 && [0, 168].includes(b)) && !(a === 100 && b >= 64 && b <= 127) &&
39
- !(a === 198 && [18, 19].includes(b));
40
- }
41
- if (isIP(address) === 6) {
42
- const normalized = address.toLowerCase();
43
- if (normalized.startsWith('::ffff:')) {
44
- const embedded = normalized.slice(7);
45
- return isIP(embedded) === 4 && publicAddress(embedded);
46
- }
47
- // Global unicast only; reject loopback, ULA, link-local and multicast.
48
- return /^[23][0-9a-f]{0,3}:/.test(normalized) && !normalized.startsWith('2001:db8:');
49
- }
50
- return false;
51
- }
52
-
53
- function endpointUrl(value) {
54
- let target;
55
- try { target = new URL(value); } catch { throw new A2AParticipantError('invalid_endpoint'); }
56
- const hostname = target.hostname.replace(/^\[|\]$/g, '').toLowerCase();
57
- if (target.protocol !== 'https:' || target.username || target.password || target.hash ||
58
- /(^|\.)(localhost|local|internal|lan|home|test|invalid)$/.test(hostname) ||
59
- (!isIP(hostname) && !hostname.includes('.')) || (isIP(hostname) && !publicAddress(hostname))) {
60
- throw new A2AParticipantError('invalid_endpoint');
61
- }
62
- return target;
63
- }
64
-
65
- function redact(value, secret) {
66
- return secret && typeof value === 'string' ? value.replaceAll(secret, '[REDACTED_SECRET]') : value;
67
- }
68
-
69
- function boundedText(value, secret, trim = true) {
70
- const text = redact(typeof value === 'string' ? value : '', secret);
71
- const result = trim ? text.trim() : text;
72
- if (Buffer.byteLength(result, 'utf8') > OUTPUT_LIMIT) throw new A2AParticipantError('output_too_large');
73
- return result;
74
- }
75
-
76
- function identifier(value, secret) {
77
- const text = boundedText(value, secret);
78
- if (!text || text.length > 256 || (secret && value.includes(secret))) throw new A2AParticipantError('response_invalid');
79
- return text;
80
- }
81
-
82
- async function abortableLookup(lookupImpl, hostname, signal) {
83
- signal?.throwIfAborted();
84
- let onAbort;
85
- const aborted = new Promise((_, reject) => {
86
- onAbort = () => reject(signal.reason);
87
- signal?.addEventListener('abort', onAbort, { once: true });
88
- });
89
- try {
90
- return await Promise.race([lookupImpl(hostname, { all: true, verbatim: true }), aborted]);
91
- } finally {
92
- signal?.removeEventListener('abort', onAbort);
93
- }
94
- }
95
-
96
- function partsText(parts) {
97
- return (parts ?? []).filter((part) => part.content?.$case === 'text')
98
- .map((part) => part.content.value).join('\n');
99
- }
100
-
101
- function usageFromMetadata(metadata) {
102
- const value = metadata?.usage;
103
- const count = (number) => Number.isSafeInteger(number) && number >= 0 ? number : null;
104
- const totalTokens = count(value?.totalTokens ?? value?.total_tokens);
105
- if (totalTokens === null) return null;
106
- return { source: 'executor', totalTokens, inputTokens: count(value.inputTokens ?? value.input_tokens),
107
- outputTokens: count(value.outputTokens ?? value.output_tokens), cachedInputTokens: null,
108
- cacheWriteInputTokens: null, reasoningOutputTokens: null };
109
- }
110
-
111
- function limitedResponse(response) {
112
- if (!response.body) throw new A2AParticipantError('response_invalid');
113
- let bytes = 0;
114
- const body = response.body.pipeThrough(new TransformStream({
115
- transform(chunk, controller) {
116
- bytes += chunk.byteLength;
117
- if (bytes > RESPONSE_LIMIT) throw new A2AParticipantError('response_too_large');
118
- controller.enqueue(chunk);
119
- }
120
- }));
121
- return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
122
- }
123
-
124
- function pinnedHttpsFetch(target, init, addresses, requestImpl) {
125
- return new Promise((resolve, reject) => {
126
- const request = requestImpl(target, {
127
- method: init.method ?? 'GET', signal: init.signal,
128
- headers: Object.fromEntries(init.headers),
129
- lookup(_hostname, options, callback) {
130
- const matching = addresses.filter((entry) => !options.family || entry.family === options.family);
131
- if (!matching.length) return callback(new A2AParticipantError('invalid_endpoint'));
132
- if (options.all) callback(null, matching);
133
- else callback(null, matching[0].address, matching[0].family);
134
- }
135
- }, (incoming) => {
136
- const headers = new Headers();
137
- for (const [name, value] of Object.entries(incoming.headers)) {
138
- for (const item of Array.isArray(value) ? value : [value]) {
139
- if (item !== undefined) headers.append(name, item);
140
- }
141
- }
142
- const bodyless = [204, 205, 304].includes(incoming.statusCode);
143
- if (bodyless) incoming.resume();
144
- resolve(new Response(bodyless ? null : Readable.toWeb(incoming), {
145
- status: incoming.statusCode, statusText: incoming.statusMessage, headers
146
- }));
147
- });
148
- request.on('error', reject);
149
- request.end(init.body);
150
- });
151
- }
152
-
153
- /** A2A SDK client adapter. Remote completion is a turn result, not Fuli task verification. */
154
- export function createA2AParticipant({
155
- url,
156
- env = process.env,
157
- fetchImpl,
158
- lookupImpl = lookup,
159
- httpsRequestImpl = httpsRequest,
160
- cardPath = '/.well-known/agent-card.json',
161
- legacyCompat = false,
162
- timeoutMs = 5 * 60 * 1000,
163
- pollIntervalMs = 500
164
- } = {}) {
165
- const secret = typeof env.FULI_A2A_TOKEN === 'string' ? env.FULI_A2A_TOKEN.trim() : '';
166
- let client = null;
167
- let sdk = null;
168
- let card = null;
169
- let contextId = '';
170
- let pendingTaskId = '';
171
- let active = null;
172
- let closed = false;
173
-
174
- async function safeFetch(input, init = {}) {
175
- const configured = endpointUrl(url);
176
- const target = endpointUrl(input instanceof Request ? input.url : String(input));
177
- if (target.origin !== configured.origin) throw new A2AParticipantError('invalid_endpoint');
178
- const hostname = target.hostname.replace(/^\[|\]$/g, '');
179
- let addresses = [{ address: hostname, family: isIP(hostname) }];
180
- if (!isIP(hostname)) {
181
- addresses = await abortableLookup(lookupImpl, hostname, init.signal);
182
- if (!addresses.length || addresses.some((item) => !publicAddress(item.address))) {
183
- throw new A2AParticipantError('invalid_endpoint');
184
- }
185
- }
186
- init.signal?.throwIfAborted();
187
- const headers = new Headers(init.headers);
188
- if (secret) headers.set('Authorization', `Bearer ${secret}`);
189
- const requestInit = { ...init, headers, redirect: 'error' };
190
- // The default transport uses the validated DNS answer rather than resolving again at connection time.
191
- const response = fetchImpl && fetchImpl !== globalThis.fetch
192
- ? await fetchImpl(target, requestInit)
193
- : await pinnedHttpsFetch(target, requestInit, addresses, httpsRequestImpl);
194
- if (response.redirected || (response.url && new URL(response.url).origin !== configured.origin)) {
195
- await response.body?.cancel();
196
- throw new A2AParticipantError('invalid_endpoint');
197
- }
198
- if (!response.ok) {
199
- await response.body?.cancel();
200
- throw new A2AParticipantError([401, 403].includes(response.status) ? 'waiting_auth' : 'remote_failed', response.status);
201
- }
202
- return limitedResponse(response);
203
- }
204
-
205
- function safeFailure(error, signal) {
206
- if (signal?.aborted) return new A2AParticipantError(signal.reason?.name === 'TimeoutError' ? 'timeout' : 'cancelled');
207
- return error instanceof A2AParticipantError ? error : new A2AParticipantError('remote_failed');
208
- }
209
-
210
- async function connect(signal) {
211
- if (closed) throw new A2AParticipantError('participant_closed');
212
- endpointUrl(url);
213
- if (secret && /[\r\n]/.test(secret)) throw new A2AParticipantError('waiting_auth');
214
- const response = await safeFetch(new URL(cardPath, url), { signal, headers: { 'A2A-Version': '1.0' } });
215
- const rawCard = await response.json();
216
- const [protocol, clients] = await Promise.all([import('@a2a-js/sdk'), import('@a2a-js/sdk/client')]);
217
- sdk = protocol;
218
- const resolver = new clients.DefaultAgentCardResolver({ legacyCompat: { enabled: legacyCompat } });
219
- card = resolver.normalizeAgentCard(rawCard);
220
- if (!card.supportedInterfaces?.length) throw new A2AParticipantError('unsupported_protocol');
221
- for (const entry of card.supportedInterfaces) {
222
- if (endpointUrl(entry.url).origin !== endpointUrl(url).origin) throw new A2AParticipantError('invalid_endpoint');
223
- }
224
- const compatible = card.supportedInterfaces.filter((entry) =>
225
- ['JSONRPC', 'HTTP+JSON'].includes(entry.protocolBinding.toUpperCase()) &&
226
- (/^1\.0(?:\.0)?$/.test(entry.protocolVersion) || (legacyCompat && /^0\.3(?:\.\d+)?$/.test(entry.protocolVersion))));
227
- if (!compatible.length) throw new A2AParticipantError('unsupported_protocol');
228
- if (card.securityRequirements?.length && !secret) throw new A2AParticipantError('waiting_auth');
229
- const factory = new clients.ClientFactory({
230
- transports: [new clients.JsonRpcTransportFactory({ fetchImpl: safeFetch, legacyCompat: { enabled: legacyCompat } }),
231
- new clients.RestTransportFactory({ fetchImpl: safeFetch, legacyCompat: { enabled: legacyCompat } })],
232
- clientConfig: { polling: true, acceptedOutputModes: ['text/plain'] }, cardResolver: resolver
233
- });
234
- client = await factory.createFromAgentCard({ ...card, supportedInterfaces: compatible });
235
- }
236
-
237
- return {
238
- describe() {
239
- return { applicationLabel: 'A2A', automatic: true, workspaceWrite: false, cancel: 'remote_request', reportsUsage: false };
240
- },
241
- async preflight({ signal } = {}) {
242
- const boundedSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs);
243
- try {
244
- if (active) throw new A2AParticipantError('participant_busy');
245
- await connect(boundedSignal);
246
- return { ready: true, applicationLabel: 'A2A', name: boundedText(card.name, secret),
247
- protocolVersion: client.protocolVersion, streaming: Boolean(card.capabilities?.streaming), evidence: 'agent_card_discovery' };
248
- } catch (error) {
249
- if (!active) client = null;
250
- const failure = safeFailure(error, boundedSignal);
251
- return { ready: false, code: failure.code, reason: failure.message };
252
- }
253
- },
254
- async dispatch({ prompt, signal, turn } = {}) {
255
- if (closed) throw new A2AParticipantError('participant_closed');
256
- if (active) throw new A2AParticipantError('participant_busy');
257
- if (typeof prompt !== 'string' || !prompt.trim()) throw new A2AParticipantError('response_invalid');
258
- const controller = new AbortController();
259
- const boundedSignal = AbortSignal.any([controller.signal, AbortSignal.timeout(timeoutMs), ...(signal ? [signal] : [])]);
260
- active = controller;
261
- let remoteTaskId = '';
262
- let cancellation = null;
263
- let task = null;
264
- let message = null;
265
- let eventCount = 0;
266
- const artifacts = new Map();
267
- const actualRecord = () => {
268
- if (!remoteTaskId && !message?.messageId) return null;
269
- const metadata = message?.metadata ?? task?.metadata;
270
- const model = typeof metadata?.model === 'string' && metadata.model.length <= 256
271
- ? redact(metadata.model, secret) : null;
272
- return { sourceApplication: 'other', applicationLabel: 'A2A', provider: 'a2a', model,
273
- sessionId: identifier(contextId || remoteTaskId || message?.messageId, secret),
274
- remoteTaskId: remoteTaskId ? identifier(remoteTaskId, secret) : null,
275
- usage: usageFromMetadata(metadata), evidence: 'remote_protocol_response' };
276
- };
277
- const requestCancellation = () => {
278
- if (!remoteTaskId || !client) return Promise.resolve(false);
279
- cancellation ??= client.cancelTask({ id: remoteTaskId, tenant: '' }, { signal: AbortSignal.timeout(5000) })
280
- .then((result) => result.id === remoteTaskId && result.status?.state === sdk.TaskState.TASK_STATE_CANCELED)
281
- .catch(() => false);
282
- return cancellation;
283
- };
284
- const onAbort = () => { void requestCancellation(); };
285
- boundedSignal.addEventListener('abort', onAbort, { once: true });
286
- const capture = (value) => {
287
- if (value.messageId) {
288
- if (value.role !== sdk.Role.ROLE_AGENT) throw new A2AParticipantError('response_invalid');
289
- identifier(value.messageId, secret);
290
- if ((value.taskId && remoteTaskId && value.taskId !== remoteTaskId) ||
291
- (value.contextId && contextId && value.contextId !== contextId)) {
292
- throw new A2AParticipantError('response_invalid');
293
- }
294
- if (value.contextId) contextId = identifier(value.contextId, secret);
295
- if (value.taskId) remoteTaskId = identifier(value.taskId, secret);
296
- message = value;
297
- boundedSignal.throwIfAborted();
298
- return;
299
- }
300
- const id = value.id ?? value.taskId;
301
- if (!id || (remoteTaskId && remoteTaskId !== id)) throw new A2AParticipantError('response_invalid');
302
- remoteTaskId = identifier(id, secret);
303
- if (value.contextId && contextId && value.contextId !== contextId) throw new A2AParticipantError('response_invalid');
304
- if (value.contextId) contextId = identifier(value.contextId, secret);
305
- task = { ...task, ...value, id, status: value.status ?? task?.status };
306
- for (const artifact of value.artifacts ?? (value.artifact ? [value.artifact] : [])) {
307
- if (!artifact.artifactId || artifacts.size >= 32 && !artifacts.has(artifact.artifactId)) {
308
- throw new A2AParticipantError('response_invalid');
309
- }
310
- const previous = artifacts.get(artifact.artifactId);
311
- const text = `${value.append ? previous?.text ?? '' : ''}${partsText(artifact.parts)}`;
312
- const name = boundedText(artifact.name, secret);
313
- artifacts.set(artifact.artifactId, { artifactId: identifier(artifact.artifactId, secret),
314
- ...(name.trim() ? { name } : {}), text: boundedText(text, secret, false) });
315
- }
316
- boundedSignal.throwIfAborted();
317
- };
318
- try {
319
- if (!client) await connect(boundedSignal);
320
- boundedSignal.throwIfAborted();
321
- const params = sdk.SendMessageRequest.fromJSON({
322
- message: { messageId: randomUUID(), role: 'ROLE_USER', contextId, taskId: pendingTaskId,
323
- parts: [{ text: prompt }], metadata: { fuli: { turnId: turn?.id ?? turn?.turnId ?? null, attemptId: turn?.attemptId ?? null } } },
324
- configuration: { acceptedOutputModes: ['text/plain'], historyLength: 8, returnImmediately: true }
325
- });
326
- if (card.capabilities?.streaming) {
327
- for await (const event of client.sendMessageStream(params, { signal: boundedSignal })) {
328
- if (++eventCount > 512 || !event.payload) throw new A2AParticipantError('response_invalid');
329
- capture(event.payload.value);
330
- if (task?.status && ![sdk.TaskState.TASK_STATE_SUBMITTED, sdk.TaskState.TASK_STATE_WORKING,
331
- sdk.TaskState.TASK_STATE_UNSPECIFIED].includes(task.status.state)) break;
332
- if (message && !remoteTaskId) break;
333
- }
334
- } else capture(await client.sendMessage(params, { signal: boundedSignal }));
335
- while ([sdk.TaskState.TASK_STATE_SUBMITTED, sdk.TaskState.TASK_STATE_WORKING].includes(task?.status?.state)) {
336
- await delay(pollIntervalMs, undefined, { signal: boundedSignal });
337
- capture(await client.getTask({ id: remoteTaskId, tenant: '', historyLength: 8 }, { signal: boundedSignal }));
338
- }
339
- const state = task?.status?.state;
340
- if (state === sdk.TaskState.TASK_STATE_INPUT_REQUIRED || state === sdk.TaskState.TASK_STATE_AUTH_REQUIRED) {
341
- pendingTaskId = remoteTaskId;
342
- const failure = new A2AParticipantError(state === sdk.TaskState.TASK_STATE_INPUT_REQUIRED ? 'waiting_input' : 'waiting_auth');
343
- failure.body = boundedText(partsText(task.status.message?.parts), secret);
344
- failure.remoteTaskId = identifier(remoteTaskId, secret);
345
- throw failure;
346
- }
347
- if ((!message || task) && state !== sdk.TaskState.TASK_STATE_COMPLETED) throw new A2AParticipantError('remote_failed');
348
- const statusMessage = task?.status?.message?.role === sdk.Role.ROLE_AGENT ? task.status.message : null;
349
- const latestMessage = message ?? statusMessage ?? [...(task?.history ?? [])].reverse().find((item) => item.role === sdk.Role.ROLE_AGENT);
350
- const artifactText = [...artifacts.values()].map((item) => item.text).filter(Boolean).join('\n');
351
- const body = boundedText(artifactText || partsText(latestMessage?.parts), secret);
352
- if (!body) throw new A2AParticipantError('response_invalid');
353
- pendingTaskId = '';
354
- return { body, actual: actualRecord(), artifacts: [...artifacts.values()] };
355
- } catch (error) {
356
- const failure = safeFailure(error, boundedSignal);
357
- const actual = actualRecord();
358
- if (actual) failure.actual = actual;
359
- if (boundedSignal.aborted) {
360
- failure.cancellationConfirmed = await requestCancellation();
361
- failure.remoteTaskId = remoteTaskId ? identifier(remoteTaskId, secret) : null;
362
- }
363
- throw failure;
364
- } finally {
365
- boundedSignal.removeEventListener('abort', onAbort);
366
- active = null;
367
- }
368
- },
369
- async close() {
370
- closed = true;
371
- active?.abort();
372
- }
373
- };
374
- }
@@ -1,24 +0,0 @@
1
- import { runParticipantProcess, parseParticipantEvents } from '../../roundtables/process.js';
2
-
3
- export function createClaudeCodeParticipant({ command = process.env.FULI_CLAUDE_BIN ?? 'claude',
4
- workspace = process.cwd(), env = process.env, model, runProcess = runParticipantProcess } = {}) {
5
- const settings = JSON.stringify({ disableAllHooks: true });
6
- const clientEnv = Object.fromEntries(Object.entries(env).filter(([key]) => !key.startsWith('FULI_')));
7
- return {
8
- async preflight({ signal } = {}) {
9
- try {
10
- const result = await runProcess(command, ['--version'], { cwd: workspace, env: clientEnv, signal, timeoutMs: 10_000 });
11
- return { ready: result.code === 0, authentication: 'checked_on_dispatch', reason: result.code === 0 ? null : 'runtime_unavailable' };
12
- } catch (error) { return { ready: false, reason: error.code ?? 'runtime_unavailable' }; }
13
- },
14
- async dispatch({ prompt, signal, allowWrite = false, resultSchema }) {
15
- const args = ['-p', '--output-format', 'stream-json', '--verbose', '--no-session-persistence',
16
- '--permission-mode', allowWrite ? 'acceptEdits' : 'dontAsk', '--no-chrome', '--disable-slash-commands',
17
- '--setting-sources', '', '--settings', settings, '--mcp-config', '{"mcpServers":{}}', '--strict-mcp-config',
18
- '--tools', allowWrite ? 'Read,Glob,Grep,Edit,Write' : 'Read,Glob,Grep',
19
- '--max-budget-usd', env.FULI_CLAUDE_MAX_BUDGET_USD ?? '1.00', ...(model ? ['--model', model] : []),
20
- ...(resultSchema ? ['--json-schema', JSON.stringify(resultSchema)] : [])];
21
- return parseParticipantEvents('claude_code', await runProcess(command, args, { cwd: workspace, env: clientEnv, input: prompt, signal }));
22
- }
23
- };
24
- }
@@ -1,32 +0,0 @@
1
- import { ROUNDTABLE_WORKER_BOUNDARY } from '../../roundtables/turn-prompt.js';
2
-
3
- // Discover the effective MCP configuration, then disable and verify every entry.
4
- // Empty TOML tables merge recursively and cannot clear inherited servers.
5
- export async function prepareCodexRoundtableConfig({ command, workspace, env, signal, runProcess,
6
- platform = process.platform }) {
7
- const options = ['--disable', 'plugins', '--disable', 'apps', '--disable', 'multi_agent',
8
- '-c', 'hooks.enabled=false', '-c', 'notify=[]', '-c', 'project_doc_max_bytes=0',
9
- '-c', `developer_instructions=${JSON.stringify(ROUNDTABLE_WORKER_BOUNDARY)}`,
10
- '-c', 'sandbox_workspace_write.writable_roots=[]', '-c', 'sandbox_workspace_write.network_access=false',
11
- '-c', 'approval_policy="never"', '-c', 'approvals_reviewer="user"',
12
- '-c', 'features.browser_use=false', '-c', 'features.computer_use=false',
13
- ...(platform === 'win32' ? ['-c', 'windows.sandbox="unelevated"'] : [])];
14
- const list = async (overrides) => {
15
- const result = await runProcess(command, ['mcp', 'list', '--json', ...overrides],
16
- { cwd: workspace, env, signal, timeoutMs: 10000 });
17
- if (result.code !== 0) throw isolationFailure();
18
- let servers;
19
- try { servers = JSON.parse(result.stdout); } catch { throw isolationFailure(); }
20
- if (!Array.isArray(servers) || servers.length > 100 || servers.some(server =>
21
- !server || !/^[A-Za-z0-9_-]{1,128}$/.test(server.name) || typeof server.enabled !== 'boolean')) throw isolationFailure();
22
- return servers;
23
- };
24
- const servers = await list(options);
25
- for (const server of servers) options.push('-c', `mcp_servers.${server.name}.enabled=false`);
26
- if ((await list(options)).some(server => server.enabled)) throw isolationFailure();
27
- return options;
28
- }
29
-
30
- function isolationFailure() {
31
- return Object.assign(new Error('Codex effective MCP isolation could not be verified'), { code: 'runtime_isolation_failed' });
32
- }
@@ -1,34 +0,0 @@
1
- import { runParticipantProcess, parseParticipantEvents } from '../../roundtables/process.js';
2
- import { mkdtempSync, writeFileSync, unlinkSync, rmdirSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { prepareCodexRoundtableConfig } from './roundtable-config.js';
6
-
7
- export function createCodexParticipant({ command = process.env.FULI_CODEX_BIN ?? 'codex',
8
- workspace = process.cwd(), env = process.env, model, reasoningEffort = env.FULI_CODEX_REASONING_EFFORT,
9
- runProcess = runParticipantProcess } = {}) {
10
- if (reasoningEffort && !['none', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(reasoningEffort)) throw new TypeError('Unsupported Codex reasoning effort');
11
- const clientEnv = Object.fromEntries(Object.entries(env).filter(([key]) => !key.startsWith('FULI_')));
12
- return {
13
- async preflight({ signal } = {}) {
14
- try {
15
- const version = await runProcess(command, ['--version'], { cwd: workspace, env: clientEnv, signal, timeoutMs: 10_000 });
16
- const auth = await runProcess(command, ['login', 'status'], { cwd: workspace, env: clientEnv, signal, timeoutMs: 10_000 });
17
- return { ready: version.code === 0 && auth.code === 0, reason: auth.code === 0 ? null : 'waiting_auth', authentication: 'local_client_configuration' };
18
- } catch (error) { return { ready: false, reason: error.code ?? 'runtime_unavailable' }; }
19
- },
20
- async dispatch({ prompt, signal, allowWrite = false, resultSchema }) {
21
- const overrides = await prepareCodexRoundtableConfig({ command, workspace, env: clientEnv, signal, runProcess });
22
- const directory = resultSchema ? mkdtempSync(join(tmpdir(), 'fuli-roundtable-schema-')) : null;
23
- const schemaPath = directory ? join(directory, 'result.json') : null;
24
- if (schemaPath) writeFileSync(schemaPath, JSON.stringify(resultSchema), { mode: 0o600 });
25
- try {
26
- const args = ['exec', '--ignore-rules', '--ephemeral', '--skip-git-repo-check',
27
- '--sandbox', allowWrite ? 'workspace-write' : 'read-only', '--color', 'never', '--json', '-C', workspace,
28
- ...overrides, ...(reasoningEffort ? ['-c', `model_reasoning_effort="${reasoningEffort}"`] : []),
29
- ...(model ? ['--model', model] : []), ...(schemaPath ? ['--output-schema', schemaPath] : []), '-'];
30
- return parseParticipantEvents('codex', await runProcess(command, args, { cwd: workspace, env: clientEnv, input: prompt, signal }));
31
- } finally { if (schemaPath) unlinkSync(schemaPath); if (directory) rmdirSync(directory); }
32
- }
33
- };
34
- }