ethagent 3.3.4 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/.claude-plugin/marketplace.json +11 -0
  2. package/.claude-plugin/plugin.json +35 -0
  3. package/LICENSE +1 -1
  4. package/README.md +64 -104
  5. package/commands/ethagent.md +40 -0
  6. package/package.json +16 -16
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -6
  8. package/src/app/keybindings/types.ts +1 -6
  9. package/src/cli/ResetConfirmView.tsx +54 -53
  10. package/src/cli/demo.ts +86 -0
  11. package/src/cli/hookIo.ts +45 -0
  12. package/src/cli/main.tsx +94 -123
  13. package/src/cli/memoryGuard.ts +49 -0
  14. package/src/cli/reset.ts +28 -70
  15. package/src/cli/sessionStart.ts +33 -0
  16. package/src/cli/status.ts +46 -0
  17. package/src/cli/sync.ts +167 -0
  18. package/src/cli/syncAdapters/claude-code.ts +86 -0
  19. package/src/cli/syncAdapters/codex.ts +66 -0
  20. package/src/cli/syncAdapters/index.ts +45 -0
  21. package/src/cli/syncAdapters/managedBlock.ts +175 -0
  22. package/src/cli/syncAdapters/shared.ts +63 -0
  23. package/src/identity/continuity/envelopeParse.ts +20 -1
  24. package/src/identity/continuity/publicSkills.ts +3 -1
  25. package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
  26. package/src/identity/continuity/skills/scaffold.ts +5 -2
  27. package/src/identity/continuity/snapshots.ts +12 -5
  28. package/src/identity/continuity/storage/defaults.ts +20 -19
  29. package/src/identity/continuity/storage/status.ts +1 -1
  30. package/src/identity/ens/ensLookup/constants.ts +1 -1
  31. package/src/identity/manager/IdentityManager.tsx +33 -0
  32. package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
  33. package/src/identity/{hub → manager}/Routes.tsx +48 -34
  34. package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
  35. package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
  36. package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
  37. package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
  38. package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
  39. package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
  40. package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
  41. package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
  42. package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
  43. package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
  44. package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
  45. package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
  46. package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
  47. package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
  48. package/src/identity/{hub → manager}/create/effects.ts +19 -10
  49. package/src/identity/manager/create/importScan.ts +122 -0
  50. package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
  51. package/src/identity/{hub → manager}/custody/actions.ts +1 -15
  52. package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
  53. package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
  54. package/src/identity/{hub → manager}/custody/types.ts +1 -2
  55. package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
  56. package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
  57. package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
  58. package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
  59. package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
  60. package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
  61. package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
  62. package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
  63. package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
  64. package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
  65. package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
  66. package/src/identity/{hub → manager}/profile/effects.ts +1 -3
  67. package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
  68. package/src/identity/{hub → manager}/profile/state.ts +1 -1
  69. package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
  70. package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
  71. package/src/identity/{hub → manager}/restore/apply.ts +1 -1
  72. package/src/identity/{hub → manager}/restore/auth.ts +1 -1
  73. package/src/identity/{hub → manager}/restore/discover.ts +1 -1
  74. package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
  75. package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
  76. package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
  77. package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
  78. package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
  79. package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
  80. package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
  81. package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
  82. package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
  83. package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
  84. package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
  85. package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
  86. package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
  87. package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
  88. package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
  89. package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
  90. package/src/identity/manager/shared/effects/types.ts +30 -0
  91. package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
  92. package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
  93. package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
  94. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
  95. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
  96. package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
  97. package/src/identity/{hub → manager}/shared/utils.ts +6 -10
  98. package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
  99. package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
  100. package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
  101. package/src/identity/{hub → manager}/types.ts +5 -6
  102. package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
  103. package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
  104. package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
  105. package/src/identity/registry/erc8004/discovery.ts +3 -17
  106. package/src/identity/registry/erc8004/utils.ts +1 -1
  107. package/src/identity/storage/ipfs.ts +21 -1
  108. package/src/identity/wallet/browserWallet/html.ts +10 -2
  109. package/src/identity/wallet/browserWallet/http.ts +18 -0
  110. package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
  111. package/src/identity/wallet/browserWallet/requests.ts +10 -28
  112. package/src/identity/wallet/browserWallet/session.ts +26 -33
  113. package/src/identity/wallet/browserWallet/validation.ts +14 -0
  114. package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
  115. package/src/identity/wallet/page/boot.ts +43 -0
  116. package/src/identity/wallet/page/config.ts +59 -0
  117. package/src/identity/wallet/page/constants.ts +12 -0
  118. package/src/identity/wallet/page/copy.ts +47 -68
  119. package/src/identity/wallet/page/css.ts +638 -0
  120. package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
  121. package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
  122. package/src/identity/wallet/page/markup.ts +44 -34
  123. package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
  124. package/src/identity/wallet/page/resize.ts +95 -0
  125. package/src/identity/wallet/page/state.ts +135 -8
  126. package/src/identity/wallet/page/timeline.ts +161 -0
  127. package/src/identity/wallet/page/view.ts +22 -302
  128. package/src/storage/config.ts +30 -80
  129. package/src/storage/reset.ts +31 -0
  130. package/src/storage/secrets.ts +1 -16
  131. package/src/ui/Select.tsx +27 -5
  132. package/src/ui/Spinner.tsx +16 -15
  133. package/src/ui/Surface.tsx +21 -17
  134. package/src/ui/TextArea.tsx +173 -0
  135. package/src/ui/TextInput.tsx +31 -133
  136. package/src/ui/theme.ts +22 -13
  137. package/src/utils/clipboard.ts +0 -140
  138. package/src/app/FirstRun.tsx +0 -577
  139. package/src/app/FirstRunTimeline.tsx +0 -51
  140. package/src/app/firstRunConfig.ts +0 -26
  141. package/src/app/hooks/useCancelRequest.ts +0 -22
  142. package/src/app/hooks/useDoublePress.ts +0 -46
  143. package/src/app/hooks/useExitOnCtrlC.ts +0 -36
  144. package/src/auth/openaiOAuth/credentials.ts +0 -47
  145. package/src/auth/openaiOAuth/crypto.ts +0 -23
  146. package/src/auth/openaiOAuth/index.ts +0 -238
  147. package/src/auth/openaiOAuth/landingPage.ts +0 -116
  148. package/src/auth/openaiOAuth/listener.ts +0 -151
  149. package/src/auth/openaiOAuth/refresh.ts +0 -70
  150. package/src/auth/openaiOAuth/shared.ts +0 -115
  151. package/src/chat/ChatBottomPane.tsx +0 -296
  152. package/src/chat/ChatScreen.tsx +0 -1685
  153. package/src/chat/ConversationStack.tsx +0 -56
  154. package/src/chat/MessageList.tsx +0 -638
  155. package/src/chat/SessionStatus.tsx +0 -53
  156. package/src/chat/chatEnvironment.ts +0 -16
  157. package/src/chat/chatScreenUtils.ts +0 -194
  158. package/src/chat/chatSessionState.ts +0 -146
  159. package/src/chat/chatTurnContext.ts +0 -50
  160. package/src/chat/chatTurnOrchestrator.ts +0 -603
  161. package/src/chat/chatTurnRows.ts +0 -64
  162. package/src/chat/commands.ts +0 -494
  163. package/src/chat/continuityEditReview.ts +0 -42
  164. package/src/chat/display/DiffView.tsx +0 -193
  165. package/src/chat/display/SyntaxText.tsx +0 -192
  166. package/src/chat/display/toolCallDisplay.ts +0 -103
  167. package/src/chat/display/toolResultDisplay.ts +0 -19
  168. package/src/chat/input/ChatInput.tsx +0 -625
  169. package/src/chat/input/chatInputHelpers.ts +0 -62
  170. package/src/chat/input/chatInputState.ts +0 -247
  171. package/src/chat/input/chatPaste.ts +0 -49
  172. package/src/chat/input/imageRefs.ts +0 -30
  173. package/src/chat/input/inputRendering.tsx +0 -93
  174. package/src/chat/input/textCursor.ts +0 -212
  175. package/src/chat/messageMarkdown.ts +0 -220
  176. package/src/chat/messageRows.ts +0 -43
  177. package/src/chat/planImplementation.ts +0 -62
  178. package/src/chat/slashCommandHandlers.ts +0 -122
  179. package/src/chat/slashCommandViews.ts +0 -120
  180. package/src/chat/transcript/TranscriptView.tsx +0 -184
  181. package/src/chat/transcript/transcriptViewport.ts +0 -295
  182. package/src/chat/views/ContextLimitView.tsx +0 -95
  183. package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
  184. package/src/chat/views/CopyPicker.tsx +0 -50
  185. package/src/chat/views/PermissionPrompt.tsx +0 -156
  186. package/src/chat/views/PermissionsView.tsx +0 -165
  187. package/src/chat/views/PlanApprovalView.tsx +0 -91
  188. package/src/chat/views/ResumeView.tsx +0 -273
  189. package/src/chat/views/RewindView.tsx +0 -412
  190. package/src/cli/preview.tsx +0 -14
  191. package/src/cli/updateNotice.ts +0 -54
  192. package/src/identity/continuity/privateEdit/apply.ts +0 -170
  193. package/src/identity/continuity/privateEdit/diff.ts +0 -6
  194. package/src/identity/continuity/privateEdit/files.ts +0 -23
  195. package/src/identity/continuity/privateEdit/types.ts +0 -28
  196. package/src/identity/continuity/privateEdit.ts +0 -46
  197. package/src/identity/hub/IdentityHub.tsx +0 -14
  198. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
  199. package/src/identity/hub/ens/effects.ts +0 -218
  200. package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
  201. package/src/identity/hub/shared/effects/types.ts +0 -53
  202. package/src/identity/hub/shared/reconciliation/index.ts +0 -14
  203. package/src/identity/wallet/page/grainient.ts +0 -278
  204. package/src/identity/wallet/page/html.ts +0 -28
  205. package/src/identity/wallet/page/styles/base.ts +0 -260
  206. package/src/identity/wallet/page/styles/components.ts +0 -262
  207. package/src/identity/wallet/page/styles/index.ts +0 -5
  208. package/src/identity/wallet/page/styles/responsive.ts +0 -247
  209. package/src/identity/wallet/page.tsx +0 -38
  210. package/src/mcp/approvals.ts +0 -113
  211. package/src/mcp/config.ts +0 -235
  212. package/src/mcp/manager.ts +0 -482
  213. package/src/mcp/managerHelpers.ts +0 -70
  214. package/src/mcp/names.ts +0 -19
  215. package/src/mcp/output.ts +0 -96
  216. package/src/models/ModelPicker.tsx +0 -1009
  217. package/src/models/catalog.ts +0 -327
  218. package/src/models/huggingface.ts +0 -712
  219. package/src/models/huggingfaceStorage.ts +0 -136
  220. package/src/models/llamacpp.ts +0 -848
  221. package/src/models/llamacppCommands.ts +0 -44
  222. package/src/models/llamacppConfig.ts +0 -34
  223. package/src/models/llamacppDiscovery.ts +0 -176
  224. package/src/models/llamacppOutput.ts +0 -65
  225. package/src/models/llamacppPreflight.ts +0 -158
  226. package/src/models/modelDisplay.ts +0 -180
  227. package/src/models/modelPickerCatalogFlow.ts +0 -56
  228. package/src/models/modelPickerCredentials.ts +0 -166
  229. package/src/models/modelPickerData.ts +0 -41
  230. package/src/models/modelPickerDisplay.tsx +0 -132
  231. package/src/models/modelPickerHfFlow.ts +0 -192
  232. package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
  233. package/src/models/modelPickerOptions.ts +0 -457
  234. package/src/models/modelPickerTypes.ts +0 -69
  235. package/src/models/modelPickerUninstallFlow.ts +0 -48
  236. package/src/models/modelPickerViewHelpers.ts +0 -174
  237. package/src/models/modelRecommendation.ts +0 -139
  238. package/src/models/providerDisplay.ts +0 -16
  239. package/src/models/runtimeDetection.ts +0 -81
  240. package/src/models/uncensoredCatalog.ts +0 -86
  241. package/src/providers/anthropic.ts +0 -290
  242. package/src/providers/contracts.ts +0 -71
  243. package/src/providers/errors.ts +0 -80
  244. package/src/providers/gemini.ts +0 -391
  245. package/src/providers/openai-chat.ts +0 -474
  246. package/src/providers/openai-responses-format.ts +0 -177
  247. package/src/providers/openai-responses.ts +0 -306
  248. package/src/providers/openaiChatWire.ts +0 -124
  249. package/src/providers/registry.ts +0 -120
  250. package/src/providers/retry.ts +0 -58
  251. package/src/providers/sse.ts +0 -93
  252. package/src/runtime/compaction.ts +0 -395
  253. package/src/runtime/cwd.ts +0 -43
  254. package/src/runtime/providerTurn.ts +0 -38
  255. package/src/runtime/sessionMode.ts +0 -55
  256. package/src/runtime/systemPrompt.ts +0 -213
  257. package/src/runtime/textToolParser.ts +0 -161
  258. package/src/runtime/toolClaimGuards.ts +0 -143
  259. package/src/runtime/toolExecution.ts +0 -304
  260. package/src/runtime/toolIntent.ts +0 -143
  261. package/src/runtime/turn.ts +0 -369
  262. package/src/runtime/turnNudges.ts +0 -223
  263. package/src/runtime/turnTypes.ts +0 -86
  264. package/src/storage/factoryReset.ts +0 -127
  265. package/src/storage/history.ts +0 -58
  266. package/src/storage/permissions.ts +0 -76
  267. package/src/storage/rewind.ts +0 -266
  268. package/src/storage/sessionExport.ts +0 -49
  269. package/src/storage/sessions.ts +0 -495
  270. package/src/tools/bashSafety.ts +0 -186
  271. package/src/tools/bashTool.ts +0 -140
  272. package/src/tools/changeDirectoryTool.ts +0 -213
  273. package/src/tools/contracts.ts +0 -192
  274. package/src/tools/deleteFileTool.ts +0 -116
  275. package/src/tools/editTool.ts +0 -165
  276. package/src/tools/editUtils.ts +0 -170
  277. package/src/tools/fileDiff.ts +0 -261
  278. package/src/tools/listDirectoryTool.ts +0 -55
  279. package/src/tools/listSkillFilesTool.ts +0 -77
  280. package/src/tools/listSkillsTool.ts +0 -68
  281. package/src/tools/mcpResourceTools.ts +0 -95
  282. package/src/tools/permissionRules.ts +0 -85
  283. package/src/tools/privateContinuityEditTool.ts +0 -187
  284. package/src/tools/privateContinuityReadTool.ts +0 -106
  285. package/src/tools/readSkillTool.ts +0 -107
  286. package/src/tools/readTool.ts +0 -85
  287. package/src/tools/registry.ts +0 -103
  288. package/src/tools/writeFileTool.ts +0 -167
  289. package/src/ui/BrandSplash.tsx +0 -133
  290. package/src/ui/terminalTitle.ts +0 -30
  291. package/src/utils/images.ts +0 -140
  292. package/src/utils/markdownSegments.ts +0 -51
  293. package/src/utils/messages.ts +0 -37
  294. package/src/utils/withRetry.ts +0 -324
  295. /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
  296. /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
  297. /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
  298. /package/src/identity/{hub → manager}/custody/state.ts +0 -0
  299. /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
  300. /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
  301. /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
  302. /package/src/identity/{hub → manager}/ens/state.ts +0 -0
  303. /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
  304. /package/src/identity/{hub → manager}/ens/types.ts +0 -0
  305. /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
  306. /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
  307. /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
  308. /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
  309. /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
  310. /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
  311. /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
  312. /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
  313. /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
  314. /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
  315. /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
  316. /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
  317. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
  318. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
  319. /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
  320. /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
  321. /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
  322. /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
@@ -0,0 +1,46 @@
1
+ import { loadConfig, type EthagentIdentity } from '../storage/config.js'
2
+ import { getIdentityStatus } from '../storage/identity.js'
3
+ import { continuityWorkingTreeStatus } from '../identity/continuity/storage/status.js'
4
+ import { listPublishedContinuitySnapshots } from '../identity/continuity/snapshots.js'
5
+ import { changedContinuitySnapshotFiles } from '../identity/manager/continuity/state.js'
6
+
7
+ function shortAddr(addr: string | undefined): string {
8
+ if (!addr) return ''
9
+ return addr.slice(0, 6) + '…' + addr.slice(-4)
10
+ }
11
+
12
+ function networkName(chainId: number | undefined): string | null {
13
+ if (chainId === 1) return 'mainnet'
14
+ if (chainId === 8453) return 'base'
15
+ return null
16
+ }
17
+
18
+ export async function runStatus(version: string): Promise<number> {
19
+ const config = await loadConfig().catch(() => null)
20
+ const status = await getIdentityStatus(config ?? undefined).catch(() => null)
21
+ if (!status?.address) {
22
+ process.stdout.write(`ethagent v${version} · no identity yet\n`)
23
+ return 0
24
+ }
25
+ const bits: string[] = [`ethagent v${version}`]
26
+ if (status.agentId) bits.push(`#${status.agentId}`)
27
+ const network = networkName(status.chainId)
28
+ if (network) bits.push(network)
29
+ bits.push(shortAddr(status.address))
30
+ const localChanges = config?.identity ? await localChangesSegment(config.identity) : null
31
+ if (localChanges) bits.push(localChanges)
32
+ process.stdout.write(bits.join(' · ') + '\n')
33
+ return 0
34
+ }
35
+
36
+ async function localChangesSegment(identity: EthagentIdentity): Promise<string | null> {
37
+ try {
38
+ const [latest] = await listPublishedContinuitySnapshots(identity, 1)
39
+ const status = await continuityWorkingTreeStatus(identity, latest)
40
+ if (status.publishState !== 'local-changes') return null
41
+ const files = changedContinuitySnapshotFiles(status)
42
+ return files.length > 0 ? `local-changes: ${files.join(', ')}` : 'local-changes'
43
+ } catch {
44
+ return null
45
+ }
46
+ }
@@ -0,0 +1,167 @@
1
+ import { loadConfig, type EthagentIdentity } from '../storage/config.js'
2
+ import { readContinuityFiles, statIfExists, writeContinuityFiles } from '../identity/continuity/storage/files.js'
3
+ import { continuityVaultRef } from '../identity/continuity/storage/paths.js'
4
+ import { continuityWorkingTreeStatus } from '../identity/continuity/storage/status.js'
5
+ import { listPublishedContinuitySnapshots } from '../identity/continuity/snapshots.js'
6
+ import { changedContinuitySnapshotFiles } from '../identity/manager/continuity/state.js'
7
+ import { listSkills } from '../identity/continuity/skills/loadSkills.js'
8
+ import { hashManagedBody, normalizeBody, reconstructVaultFile, sectionKey } from './syncAdapters/managedBlock.js'
9
+ import { hookFilePath, readHookPayload, samePath } from './hookIo.js'
10
+ import {
11
+ BUILT_IN_ADAPTERS,
12
+ type SyncAdapter,
13
+ type SyncContext,
14
+ } from './syncAdapters/index.js'
15
+ import type { PublicSkill } from './syncAdapters/shared.js'
16
+
17
+ export type SyncOptions = { quiet?: boolean }
18
+
19
+ export async function runSync(opts: SyncOptions = {}): Promise<number> {
20
+ const config = await loadConfig()
21
+ if (!config?.identity) return 0
22
+
23
+ let all: Awaited<ReturnType<typeof listSkills>>
24
+ try {
25
+ all = await listSkills(config.identity)
26
+ } catch (err) {
27
+ process.stderr.write(`ethagent: could not load skills, skipping sync to avoid removing managed files (${(err as Error).message})\n`)
28
+ return 1
29
+ }
30
+ const publicSkills: PublicSkill[] = all.filter(s => s.visibility === 'public')
31
+
32
+ const targets: SyncAdapter[] = []
33
+ for (const adapter of BUILT_IN_ADAPTERS) {
34
+ if (await adapter.detect().catch(() => false)) targets.push(adapter)
35
+ }
36
+ if (targets.length === 0) {
37
+ if (!opts.quiet) process.stdout.write('ethagent: no harness detected\n')
38
+ return 0
39
+ }
40
+
41
+ let context: SyncContext
42
+ let pulled: string[] = []
43
+ try {
44
+ const reconciled = await reconcileSoulMemory(config.identity, targets)
45
+ context = { soul: reconciled.soul, memory: reconciled.memory }
46
+ pulled = reconciled.pulled
47
+ } catch (err) {
48
+ process.stderr.write(`ethagent: could not reconcile soul/memory, skipping sync to avoid overwriting harness files (${(err as Error).message})\n`)
49
+ return 1
50
+ }
51
+
52
+ const summaries: string[] = []
53
+ for (const adapter of targets) {
54
+ try {
55
+ const { count, skipped } = await adapter.mirror(publicSkills, context)
56
+ let summary = `${adapter.name}: ${count} skill${count === 1 ? '' : 's'}`
57
+ if (skipped > 0) summary += `, skipped ${skipped} unmanaged`
58
+ summaries.push(summary)
59
+ } catch (err) {
60
+ summaries.push(`${adapter.name}: failed (${(err as Error).message})`)
61
+ }
62
+ }
63
+ if (!opts.quiet) {
64
+ process.stdout.write(`ethagent: synced -> ${summaries.join(' | ')}\n`)
65
+ if (pulled.length > 0) {
66
+ process.stdout.write(`ethagent: pulled ${pulled.join(', ')} drift from your harness into the vault\n`)
67
+ }
68
+ }
69
+ if (!opts.quiet) await reportLocalChanges(config.identity)
70
+ return 0
71
+ }
72
+
73
+ async function reportLocalChanges(identity: EthagentIdentity): Promise<void> {
74
+ try {
75
+ const [latest] = await listPublishedContinuitySnapshots(identity, 1)
76
+ const status = await continuityWorkingTreeStatus(identity, latest)
77
+ if (status.publishState !== 'local-changes') return
78
+ const files = changedContinuitySnapshotFiles(status)
79
+ const detail = files.length > 0 ? ` (${files.join(', ')})` : ''
80
+ process.stdout.write(`ethagent: local changes detected${detail}, run 'ethagent' and Save Snapshot to back up\n`)
81
+ } catch {}
82
+ }
83
+
84
+ export async function reconcileSoulMemory(
85
+ identity: EthagentIdentity,
86
+ adapters: SyncAdapter[],
87
+ ): Promise<SyncContext & { pulled: string[] }> {
88
+ const files = await readContinuityFiles(identity)
89
+ const ref = continuityVaultRef(identity)
90
+ const [soulStat, memoryStat] = await Promise.all([
91
+ statIfExists(ref.soulPath),
92
+ statIfExists(ref.memoryPath),
93
+ ])
94
+
95
+ const reads = await Promise.all(
96
+ adapters.map(adapter => (adapter.readManaged ? adapter.readManaged().catch(() => null) : Promise.resolve(null))),
97
+ )
98
+
99
+ const soulPick = soulStat
100
+ ? pickNewest(files['SOUL.md'], soulStat.mtimeMs, reads.map(r => ({ body: r?.soul ?? null, mtimeMs: r?.mtimeMs ?? 0, lastPushedHash: r?.lastSoulHash })), '# SOUL.md')
101
+ : { content: files['SOUL.md'], pulled: false }
102
+ const memoryPick = memoryStat
103
+ ? pickNewest(files['MEMORY.md'], memoryStat.mtimeMs, reads.map(r => ({ body: r?.memory ?? null, mtimeMs: r?.mtimeMs ?? 0, lastPushedHash: r?.lastMemoryHash })), '# MEMORY.md')
104
+ : { content: files['MEMORY.md'], pulled: false }
105
+
106
+ const pulled: string[] = []
107
+ if (soulPick.pulled) pulled.push('SOUL.md')
108
+ if (memoryPick.pulled) pulled.push('MEMORY.md')
109
+ if (pulled.length > 0) {
110
+ await writeContinuityFiles(identity, { 'SOUL.md': soulPick.content, 'MEMORY.md': memoryPick.content })
111
+ }
112
+ return { soul: soulPick.content, memory: memoryPick.content, pulled }
113
+ }
114
+
115
+ function pickNewest(
116
+ vaultContent: string,
117
+ vaultMtimeMs: number,
118
+ candidates: Array<{ body: string | null; mtimeMs: number; lastPushedHash?: string }>,
119
+ fallbackHeader: string,
120
+ ): { content: string; pulled: boolean } {
121
+ const vaultKey = sectionKey(vaultContent)
122
+ let winningBody: string | null = null
123
+ let winningMtime = vaultMtimeMs
124
+ for (const candidate of candidates) {
125
+ if (candidate.body === null) continue
126
+ if (normalizeBody(candidate.body) === vaultKey) continue
127
+ if (candidate.lastPushedHash && hashManagedBody(candidate.body) === candidate.lastPushedHash) continue
128
+ if (candidate.mtimeMs > winningMtime) {
129
+ winningMtime = candidate.mtimeMs
130
+ winningBody = candidate.body
131
+ }
132
+ }
133
+ if (winningBody === null) return { content: vaultContent, pulled: false }
134
+ return { content: reconstructVaultFile(vaultContent, winningBody, fallbackHeader), pulled: true }
135
+ }
136
+
137
+ export async function runSyncList(): Promise<number> {
138
+ for (const adapter of BUILT_IN_ADAPTERS) {
139
+ const detected = await adapter.detect().catch(() => false)
140
+ const mark = detected ? 'detected' : 'not detected'
141
+ process.stdout.write(` ${adapter.name.padEnd(14)} ${mark.padEnd(13)} ${adapter.description}\n`)
142
+ }
143
+ return 0
144
+ }
145
+
146
+ export async function runSyncOnEdit(): Promise<number> {
147
+ const editedPath = await readEditedFilePathFromStdin()
148
+ if (!editedPath) return 0
149
+ const config = await loadConfig()
150
+ if (!config?.identity) return 0
151
+ if (!isManagedCorePath(config.identity, editedPath)) return 0
152
+ return runSync({ quiet: true })
153
+ }
154
+
155
+ export function isManagedCorePath(identity: EthagentIdentity, editedPath: string): boolean {
156
+ const managed: string[] = []
157
+ for (const adapter of BUILT_IN_ADAPTERS) {
158
+ if (adapter.managedFilePaths) managed.push(...adapter.managedFilePaths())
159
+ }
160
+ const ref = continuityVaultRef(identity)
161
+ managed.push(ref.soulPath, ref.memoryPath)
162
+ return managed.some(candidate => samePath(candidate, editedPath))
163
+ }
164
+
165
+ async function readEditedFilePathFromStdin(): Promise<string | null> {
166
+ return hookFilePath(await readHookPayload())
167
+ }
@@ -0,0 +1,86 @@
1
+ import fs from 'node:fs/promises'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { MANIFEST_FILE, mirrorAsSkillFolders, pathExists, readManifest, type PublicSkill } from './shared.js'
5
+ import { injectManagedBlock, readManagedContext, renderManagedBlock, writeManagedSyncState } from './managedBlock.js'
6
+ import type { ManagedRead, SyncContext } from './index.js'
7
+
8
+ function claudeDir(): string {
9
+ return path.join(os.homedir(), '.claude')
10
+ }
11
+
12
+ function claudeSkillsDir(): string {
13
+ return path.join(claudeDir(), 'skills')
14
+ }
15
+
16
+ function claudeMdPath(): string {
17
+ return path.join(claudeDir(), 'CLAUDE.md')
18
+ }
19
+
20
+ function claudeProjectMemoryMdPath(): string {
21
+ const slug = process.cwd().replace(/[:\\\/]/g, '-')
22
+ return path.join(claudeDir(), 'projects', slug, 'memory', 'MEMORY.md')
23
+ }
24
+
25
+ // The Claude Code native per-project memory directory for the current project.
26
+ // ethagent's portable memory supersedes this; the --memory-guard hook redirects
27
+ // the model away from writing here so nothing siloes on one machine.
28
+ export function claudeCodeNativeMemoryDir(): string {
29
+ return path.dirname(claudeProjectMemoryMdPath())
30
+ }
31
+
32
+ // Every project's mirrored MEMORY.md under a given ~/.claude root, across all
33
+ // directories the agent has ever been synced in, not just the current cwd.
34
+ // Reset uses this so no project is left whispering a stale ethagent block.
35
+ export async function projectMemoryMirrorsUnder(claudeRoot: string): Promise<string[]> {
36
+ const projectsDir = path.join(claudeRoot, 'projects')
37
+ let slugs: string[]
38
+ try {
39
+ slugs = await fs.readdir(projectsDir)
40
+ } catch {
41
+ return []
42
+ }
43
+ const mirrors: string[] = []
44
+ for (const slug of slugs) {
45
+ const file = path.join(projectsDir, slug, 'memory', 'MEMORY.md')
46
+ if (await pathExists(file)) mirrors.push(file)
47
+ }
48
+ return mirrors
49
+ }
50
+
51
+ export const claudeCodeAdapter = {
52
+ name: 'claude-code' as const,
53
+ description: 'Mirror public skills into ~/.claude/skills and inject soul/memory into ~/.claude/CLAUDE.md and the project MEMORY.md.',
54
+ async detect(): Promise<boolean> {
55
+ return pathExists(claudeDir())
56
+ },
57
+ async readManaged(): Promise<ManagedRead | null> {
58
+ return readManagedContext(claudeMdPath())
59
+ },
60
+ managedFilePaths(): string[] {
61
+ return [claudeMdPath()]
62
+ },
63
+ async resetManagedFilePaths(): Promise<string[]> {
64
+ return [claudeMdPath(), ...(await projectMemoryMirrorsUnder(claudeDir()))]
65
+ },
66
+ async cleanup(): Promise<void> {
67
+ const skillsDir = claudeSkillsDir()
68
+ const manifest = await readManifest(skillsDir)
69
+ await Promise.all(
70
+ manifest.skills.map(name =>
71
+ fs.rm(path.join(skillsDir, name), { recursive: true, force: true }).catch(() => {})
72
+ )
73
+ )
74
+ await fs.rm(path.join(skillsDir, MANIFEST_FILE), { force: true }).catch(() => {})
75
+ },
76
+ async mirror(skills: PublicSkill[], context?: SyncContext): Promise<{ count: number; skipped: number }> {
77
+ const result = await mirrorAsSkillFolders(claudeSkillsDir(), skills)
78
+ if (context) {
79
+ for (const target of [claudeMdPath(), claudeProjectMemoryMdPath()]) {
80
+ await injectManagedBlock(target, renderManagedBlock(context))
81
+ }
82
+ await writeManagedSyncState(claudeMdPath(), context)
83
+ }
84
+ return result
85
+ },
86
+ }
@@ -0,0 +1,66 @@
1
+ import fs from 'node:fs/promises'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { parseSkillFile } from '../../identity/continuity/skills/frontmatter.js'
5
+ import { pathExists, type PublicSkill } from './shared.js'
6
+ import { injectManagedBlock, readManagedContext, renderManagedBlock, writeManagedSyncState } from './managedBlock.js'
7
+ import type { ManagedRead, SyncContext } from './index.js'
8
+
9
+ function codexDir(): string {
10
+ return path.join(os.homedir(), '.codex')
11
+ }
12
+
13
+ function agentsFilePath(): string {
14
+ return path.join(codexDir(), 'AGENTS.md')
15
+ }
16
+
17
+ type EnrichedSkill = PublicSkill & { body: string }
18
+
19
+ async function enrichSkills(skills: PublicSkill[]): Promise<EnrichedSkill[]> {
20
+ const out: EnrichedSkill[] = []
21
+ for (const skill of skills) {
22
+ try {
23
+ const raw = await fs.readFile(skill.absolutePath, 'utf8')
24
+ const { body } = parseSkillFile(raw)
25
+ out.push({ ...skill, body })
26
+ } catch {}
27
+ }
28
+ return out
29
+ }
30
+
31
+ function neutralizeManagedMarkers(text: string): string {
32
+ return text.replace(/<!--\s*ethagent:/gi, '<!-- ethagent ')
33
+ }
34
+
35
+ function renderSkillsText(skills: EnrichedSkill[]): string {
36
+ if (skills.length === 0) return '_no public skills published yet._'
37
+ const lines: string[] = []
38
+ for (const skill of skills) {
39
+ lines.push(`## ${neutralizeManagedMarkers(skill.displayName ?? skill.name)}`, '')
40
+ if (skill.description) lines.push(neutralizeManagedMarkers(skill.description), '')
41
+ if (skill.body) lines.push(neutralizeManagedMarkers(skill.body), '')
42
+ }
43
+ return lines.join('\n').trim()
44
+ }
45
+
46
+ export const codexAdapter = {
47
+ name: 'codex' as const,
48
+ description: 'Merge soul, memory, and public skill content into ~/.codex/AGENTS.md between ethagent markers.',
49
+ async detect(): Promise<boolean> {
50
+ return pathExists(path.join(codexDir(), 'config.toml'))
51
+ },
52
+ async readManaged(): Promise<ManagedRead | null> {
53
+ return readManagedContext(agentsFilePath())
54
+ },
55
+ managedFilePaths(): string[] {
56
+ return [agentsFilePath()]
57
+ },
58
+ async mirror(skills: PublicSkill[], context?: SyncContext): Promise<{ count: number; skipped: number }> {
59
+ await fs.mkdir(codexDir(), { recursive: true })
60
+ const enriched = await enrichSkills(skills)
61
+ const block = renderManagedBlock(context, renderSkillsText(enriched))
62
+ await injectManagedBlock(agentsFilePath(), block)
63
+ if (context) await writeManagedSyncState(agentsFilePath(), context)
64
+ return { count: enriched.length, skipped: 0 }
65
+ },
66
+ }
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs/promises'
2
+ import type { PublicSkill } from './shared.js'
3
+ import { claudeCodeAdapter } from './claude-code.js'
4
+ import { codexAdapter } from './codex.js'
5
+ import { removeManagedBlock } from './managedBlock.js'
6
+
7
+ export type SyncContext = {
8
+ soul: string
9
+ memory: string
10
+ }
11
+
12
+ export type ManagedRead = {
13
+ soul: string | null
14
+ memory: string | null
15
+ mtimeMs: number
16
+ lastSoulHash?: string
17
+ lastMemoryHash?: string
18
+ }
19
+
20
+ export type SyncAdapter = {
21
+ name: string
22
+ description: string
23
+ detect: () => Promise<boolean>
24
+ readManaged?: () => Promise<ManagedRead | null>
25
+ managedFilePaths?: () => string[]
26
+ resetManagedFilePaths?: () => Promise<string[]>
27
+ cleanup?: () => Promise<void>
28
+ mirror: (skills: PublicSkill[], context?: SyncContext) => Promise<{ count: number; skipped: number }>
29
+ }
30
+
31
+ export const BUILT_IN_ADAPTERS: SyncAdapter[] = [claudeCodeAdapter, codexAdapter]
32
+
33
+ export async function clearHarnessManagedBlocks(): Promise<string[]> {
34
+ const cleared: string[] = []
35
+ for (const adapter of BUILT_IN_ADAPTERS) {
36
+ const paths = adapter.resetManagedFilePaths
37
+ ? await adapter.resetManagedFilePaths().catch(() => [])
38
+ : (adapter.managedFilePaths?.() ?? [])
39
+ for (const filePath of paths) {
40
+ if (await removeManagedBlock(filePath).catch(() => false)) cleared.push(filePath)
41
+ }
42
+ await adapter.cleanup?.().catch(() => {})
43
+ }
44
+ return cleared
45
+ }
@@ -0,0 +1,175 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { createHash } from 'node:crypto'
4
+ import { ensureTrailingNewline } from '../../identity/continuity/storage/files.js'
5
+ import { normalizeSnapshotContent } from '../../identity/continuity/storage/status.js'
6
+ import type { ManagedRead, SyncContext } from './index.js'
7
+
8
+ const SYNC_STATE_FILE = '.ethagent-sync.json'
9
+
10
+ export const START = '<!-- ethagent:start -->'
11
+ export const END = '<!-- ethagent:end -->'
12
+ const MANAGED_NOTE = '<!-- managed by ethagent; edits between these markers are pulled into your vault on `ethagent --sync`. -->'
13
+ const SOUL_START = '<!-- ethagent:soul:start -->'
14
+ const SOUL_END = '<!-- ethagent:soul:end -->'
15
+ const MEMORY_START = '<!-- ethagent:memory:start -->'
16
+ const MEMORY_END = '<!-- ethagent:memory:end -->'
17
+
18
+ function stripFirstHeader(value: string): string {
19
+ return value.replace(/^#[^\n]*\n/, '')
20
+ }
21
+
22
+ export function sectionKey(fileContent: string): string {
23
+ return normalizeSnapshotContent(stripFirstHeader(fileContent).trim())
24
+ }
25
+
26
+ export function normalizeBody(body: string): string {
27
+ return normalizeSnapshotContent(body.trim())
28
+ }
29
+
30
+ export function renderManagedBlock(context: SyncContext | undefined, extra?: string): string {
31
+ const lines: string[] = [START, MANAGED_NOTE, '']
32
+ if (context) {
33
+ const soul = stripFirstHeader(context.soul).trim()
34
+ lines.push(SOUL_START)
35
+ if (soul) lines.push(soul)
36
+ lines.push(SOUL_END, '')
37
+ const memory = stripFirstHeader(context.memory).trim()
38
+ lines.push(MEMORY_START)
39
+ if (memory) lines.push(memory)
40
+ lines.push(MEMORY_END, '')
41
+ }
42
+ const tail = extra?.trim()
43
+ if (tail) lines.push(tail, '')
44
+ lines.push(END)
45
+ return lines.join('\n')
46
+ }
47
+
48
+ export async function injectManagedBlock(filePath: string, block: string): Promise<void> {
49
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
50
+ let existing = ''
51
+ try { existing = await fs.readFile(filePath, 'utf8') } catch {}
52
+
53
+ let next: string
54
+ const startIdx = existing.indexOf(START)
55
+ const endIdx = existing.indexOf(END)
56
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
57
+ const before = existing.slice(0, startIdx).replace(/\n+$/, '')
58
+ const after = existing.slice(endIdx + END.length).replace(/^\n+/, '')
59
+ const parts: string[] = []
60
+ if (before) parts.push(before, '')
61
+ parts.push(block)
62
+ if (after) parts.push('', after)
63
+ next = parts.join('\n').replace(/\n{3,}/g, '\n\n') + (existing.endsWith('\n') ? '\n' : '')
64
+ } else {
65
+ next = existing
66
+ ? `${existing.replace(/\n+$/, '')}\n\n${block}\n`
67
+ : `${block}\n`
68
+ }
69
+
70
+ await fs.writeFile(filePath, next, 'utf8')
71
+ }
72
+
73
+ export async function removeManagedBlock(filePath: string): Promise<boolean> {
74
+ let existing: string
75
+ try { existing = await fs.readFile(filePath, 'utf8') } catch { return false }
76
+
77
+ const startIdx = existing.indexOf(START)
78
+ const endIdx = existing.indexOf(END)
79
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return false
80
+
81
+ const before = existing.slice(0, startIdx).replace(/\n+$/, '')
82
+ const after = existing.slice(endIdx + END.length).replace(/^\n+/, '')
83
+ const remainder = [before, after].filter(Boolean).join('\n\n')
84
+
85
+ if (remainder.trim() === '') {
86
+ await fs.rm(filePath, { force: true })
87
+ } else {
88
+ await fs.writeFile(filePath, `${remainder}\n`, 'utf8')
89
+ }
90
+ await fs.rm(syncStatePath(filePath), { force: true })
91
+ return true
92
+ }
93
+
94
+ export function extractOutsideManaged(fileContent: string): string {
95
+ const startIdx = fileContent.indexOf(START)
96
+ const endIdx = fileContent.indexOf(END)
97
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
98
+ const before = fileContent.slice(0, startIdx).replace(/\n+$/, '')
99
+ const after = fileContent.slice(endIdx + END.length).replace(/^\n+/, '')
100
+ return [before, after].filter(Boolean).join('\n\n').trim()
101
+ }
102
+ return fileContent.trim()
103
+ }
104
+
105
+ function extractBetween(text: string, start: string, end: string): string | null {
106
+ const startIdx = text.indexOf(start)
107
+ if (startIdx === -1) return null
108
+ const endIdx = text.indexOf(end, startIdx + start.length)
109
+ if (endIdx === -1) return null
110
+ return text.slice(startIdx + start.length, endIdx).trim()
111
+ }
112
+
113
+ export function parseManagedContext(fileContent: string): { soul: string | null; memory: string | null } {
114
+ return {
115
+ soul: extractBetween(fileContent, SOUL_START, SOUL_END),
116
+ memory: extractBetween(fileContent, MEMORY_START, MEMORY_END),
117
+ }
118
+ }
119
+
120
+ export async function readManagedContext(filePath: string): Promise<ManagedRead | null> {
121
+ let content: string
122
+ let mtimeMs: number
123
+ try {
124
+ content = await fs.readFile(filePath, 'utf8')
125
+ mtimeMs = (await fs.stat(filePath)).mtimeMs
126
+ } catch {
127
+ return null
128
+ }
129
+ const { soul, memory } = parseManagedContext(content)
130
+ const { soulHash, memoryHash } = await readManagedSyncState(filePath)
131
+ return {
132
+ soul,
133
+ memory,
134
+ mtimeMs,
135
+ ...(soulHash ? { lastSoulHash: soulHash } : {}),
136
+ ...(memoryHash ? { lastMemoryHash: memoryHash } : {}),
137
+ }
138
+ }
139
+
140
+ export function hashManagedBody(body: string): string {
141
+ return createHash('sha256').update(normalizeBody(body).replace(/\n{3,}/g, '\n\n'), 'utf8').digest('hex')
142
+ }
143
+
144
+ function syncStatePath(managedFilePath: string): string {
145
+ return path.join(path.dirname(managedFilePath), SYNC_STATE_FILE)
146
+ }
147
+
148
+ export async function writeManagedSyncState(managedFilePath: string, context: SyncContext): Promise<void> {
149
+ const state = {
150
+ soulHash: hashManagedBody(stripFirstHeader(context.soul)),
151
+ memoryHash: hashManagedBody(stripFirstHeader(context.memory)),
152
+ }
153
+ try {
154
+ await fs.writeFile(syncStatePath(managedFilePath), `${JSON.stringify(state)}\n`, 'utf8')
155
+ } catch {
156
+ }
157
+ }
158
+
159
+ async function readManagedSyncState(managedFilePath: string): Promise<{ soulHash?: string; memoryHash?: string }> {
160
+ try {
161
+ const parsed = JSON.parse(await fs.readFile(syncStatePath(managedFilePath), 'utf8')) as Record<string, unknown>
162
+ return {
163
+ soulHash: typeof parsed.soulHash === 'string' ? parsed.soulHash : undefined,
164
+ memoryHash: typeof parsed.memoryHash === 'string' ? parsed.memoryHash : undefined,
165
+ }
166
+ } catch {
167
+ return {}
168
+ }
169
+ }
170
+
171
+ export function reconstructVaultFile(currentVaultContent: string, newBody: string, fallbackHeader: string): string {
172
+ const headerMatch = currentVaultContent.match(/^#[^\n]*\n/)
173
+ const header = headerMatch ? headerMatch[0] : `${fallbackHeader}\n`
174
+ return ensureTrailingNewline(`${header}\n${newBody.trim()}\n`)
175
+ }
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import type { SkillIndexEntry } from '../../identity/continuity/skills/types.js'
4
+
5
+ export type PublicSkill = SkillIndexEntry
6
+
7
+ export const MANIFEST_FILE = '.ethagent-managed.json'
8
+
9
+ export type Manifest = {
10
+ version: 1
11
+ managedAt: string
12
+ skills: string[]
13
+ }
14
+
15
+ export async function readManifest(root: string): Promise<Manifest> {
16
+ try {
17
+ const raw = await fs.readFile(path.join(root, MANIFEST_FILE), 'utf8')
18
+ const parsed = JSON.parse(raw) as Manifest
19
+ if (parsed.version === 1 && Array.isArray(parsed.skills)) return parsed
20
+ } catch {}
21
+ return { version: 1, managedAt: new Date(0).toISOString(), skills: [] }
22
+ }
23
+
24
+ export async function writeManifest(root: string, owned: string[]): Promise<void> {
25
+ const next: Manifest = { version: 1, managedAt: new Date().toISOString(), skills: owned }
26
+ await fs.writeFile(path.join(root, MANIFEST_FILE), JSON.stringify(next, null, 2) + '\n', 'utf8')
27
+ }
28
+
29
+ export async function pathExists(file: string): Promise<boolean> {
30
+ try { await fs.access(file); return true } catch { return false }
31
+ }
32
+
33
+ export async function mirrorAsSkillFolders(
34
+ root: string,
35
+ skills: PublicSkill[],
36
+ ): Promise<{ count: number; skipped: number }> {
37
+ await fs.mkdir(root, { recursive: true })
38
+ const manifest = await readManifest(root)
39
+ const incoming = new Set(skills.map(s => s.name))
40
+ const owned: string[] = []
41
+ let skipped = 0
42
+ for (const skill of skills) {
43
+ const targetDir = path.join(root, skill.name)
44
+ const targetFile = path.join(targetDir, 'SKILL.md')
45
+ const exists = await pathExists(targetDir)
46
+ const isOurs = manifest.skills.includes(skill.name)
47
+ if (exists && !isOurs) { skipped++; continue }
48
+ try {
49
+ const body = await fs.readFile(skill.absolutePath, 'utf8')
50
+ await fs.mkdir(targetDir, { recursive: true })
51
+ await fs.writeFile(targetFile, body, 'utf8')
52
+ owned.push(skill.name)
53
+ } catch {}
54
+ }
55
+ const keep = new Set<string>(owned)
56
+ for (const name of manifest.skills) if (incoming.has(name)) keep.add(name)
57
+ for (const stale of manifest.skills) {
58
+ if (keep.has(stale)) continue
59
+ await fs.rm(path.join(root, stale), { recursive: true, force: true }).catch(() => null)
60
+ }
61
+ await writeManifest(root, [...keep])
62
+ return { count: owned.length, skipped }
63
+ }