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.
- package/.claude-plugin/marketplace.json +11 -0
- package/.claude-plugin/plugin.json +35 -0
- package/LICENSE +1 -1
- package/README.md +64 -104
- package/commands/ethagent.md +40 -0
- package/package.json +16 -16
- package/src/app/keybindings/KeybindingProvider.tsx +1 -6
- package/src/app/keybindings/types.ts +1 -6
- package/src/cli/ResetConfirmView.tsx +54 -53
- package/src/cli/demo.ts +86 -0
- package/src/cli/hookIo.ts +45 -0
- package/src/cli/main.tsx +94 -123
- package/src/cli/memoryGuard.ts +49 -0
- package/src/cli/reset.ts +28 -70
- package/src/cli/sessionStart.ts +33 -0
- package/src/cli/status.ts +46 -0
- package/src/cli/sync.ts +167 -0
- package/src/cli/syncAdapters/claude-code.ts +86 -0
- package/src/cli/syncAdapters/codex.ts +66 -0
- package/src/cli/syncAdapters/index.ts +45 -0
- package/src/cli/syncAdapters/managedBlock.ts +175 -0
- package/src/cli/syncAdapters/shared.ts +63 -0
- package/src/identity/continuity/envelopeParse.ts +20 -1
- package/src/identity/continuity/publicSkills.ts +3 -1
- package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
- package/src/identity/continuity/skills/scaffold.ts +5 -2
- package/src/identity/continuity/snapshots.ts +12 -5
- package/src/identity/continuity/storage/defaults.ts +20 -19
- package/src/identity/continuity/storage/status.ts +1 -1
- package/src/identity/ens/ensLookup/constants.ts +1 -1
- package/src/identity/manager/IdentityManager.tsx +33 -0
- package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
- package/src/identity/{hub → manager}/Routes.tsx +48 -34
- package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
- package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
- package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
- package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
- package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
- package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
- package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
- package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
- package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
- package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
- package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
- package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
- package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
- package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
- package/src/identity/{hub → manager}/create/effects.ts +19 -10
- package/src/identity/manager/create/importScan.ts +122 -0
- package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
- package/src/identity/{hub → manager}/custody/actions.ts +1 -15
- package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
- package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
- package/src/identity/{hub → manager}/custody/types.ts +1 -2
- package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
- package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
- package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
- package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
- package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
- package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
- package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
- package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
- package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
- package/src/identity/{hub → manager}/profile/effects.ts +1 -3
- package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
- package/src/identity/{hub → manager}/profile/state.ts +1 -1
- package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
- package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
- package/src/identity/{hub → manager}/restore/apply.ts +1 -1
- package/src/identity/{hub → manager}/restore/auth.ts +1 -1
- package/src/identity/{hub → manager}/restore/discover.ts +1 -1
- package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
- package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
- package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
- package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
- package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
- package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
- package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
- package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
- package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
- package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
- package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
- package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
- package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
- package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
- package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
- package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
- package/src/identity/manager/shared/effects/types.ts +30 -0
- package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
- package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
- package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
- package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
- package/src/identity/{hub → manager}/shared/utils.ts +6 -10
- package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
- package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
- package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
- package/src/identity/{hub → manager}/types.ts +5 -6
- package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
- package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
- package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
- package/src/identity/registry/erc8004/discovery.ts +3 -17
- package/src/identity/registry/erc8004/utils.ts +1 -1
- package/src/identity/storage/ipfs.ts +21 -1
- package/src/identity/wallet/browserWallet/html.ts +10 -2
- package/src/identity/wallet/browserWallet/http.ts +18 -0
- package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
- package/src/identity/wallet/browserWallet/requests.ts +10 -28
- package/src/identity/wallet/browserWallet/session.ts +26 -33
- package/src/identity/wallet/browserWallet/validation.ts +14 -0
- package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
- package/src/identity/wallet/page/boot.ts +43 -0
- package/src/identity/wallet/page/config.ts +59 -0
- package/src/identity/wallet/page/constants.ts +12 -0
- package/src/identity/wallet/page/copy.ts +47 -68
- package/src/identity/wallet/page/css.ts +638 -0
- package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
- package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
- package/src/identity/wallet/page/markup.ts +44 -34
- package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
- package/src/identity/wallet/page/resize.ts +95 -0
- package/src/identity/wallet/page/state.ts +135 -8
- package/src/identity/wallet/page/timeline.ts +161 -0
- package/src/identity/wallet/page/view.ts +22 -302
- package/src/storage/config.ts +30 -80
- package/src/storage/reset.ts +31 -0
- package/src/storage/secrets.ts +1 -16
- package/src/ui/Select.tsx +27 -5
- package/src/ui/Spinner.tsx +16 -15
- package/src/ui/Surface.tsx +21 -17
- package/src/ui/TextArea.tsx +173 -0
- package/src/ui/TextInput.tsx +31 -133
- package/src/ui/theme.ts +22 -13
- package/src/utils/clipboard.ts +0 -140
- package/src/app/FirstRun.tsx +0 -577
- package/src/app/FirstRunTimeline.tsx +0 -51
- package/src/app/firstRunConfig.ts +0 -26
- package/src/app/hooks/useCancelRequest.ts +0 -22
- package/src/app/hooks/useDoublePress.ts +0 -46
- package/src/app/hooks/useExitOnCtrlC.ts +0 -36
- package/src/auth/openaiOAuth/credentials.ts +0 -47
- package/src/auth/openaiOAuth/crypto.ts +0 -23
- package/src/auth/openaiOAuth/index.ts +0 -238
- package/src/auth/openaiOAuth/landingPage.ts +0 -116
- package/src/auth/openaiOAuth/listener.ts +0 -151
- package/src/auth/openaiOAuth/refresh.ts +0 -70
- package/src/auth/openaiOAuth/shared.ts +0 -115
- package/src/chat/ChatBottomPane.tsx +0 -296
- package/src/chat/ChatScreen.tsx +0 -1685
- package/src/chat/ConversationStack.tsx +0 -56
- package/src/chat/MessageList.tsx +0 -638
- package/src/chat/SessionStatus.tsx +0 -53
- package/src/chat/chatEnvironment.ts +0 -16
- package/src/chat/chatScreenUtils.ts +0 -194
- package/src/chat/chatSessionState.ts +0 -146
- package/src/chat/chatTurnContext.ts +0 -50
- package/src/chat/chatTurnOrchestrator.ts +0 -603
- package/src/chat/chatTurnRows.ts +0 -64
- package/src/chat/commands.ts +0 -494
- package/src/chat/continuityEditReview.ts +0 -42
- package/src/chat/display/DiffView.tsx +0 -193
- package/src/chat/display/SyntaxText.tsx +0 -192
- package/src/chat/display/toolCallDisplay.ts +0 -103
- package/src/chat/display/toolResultDisplay.ts +0 -19
- package/src/chat/input/ChatInput.tsx +0 -625
- package/src/chat/input/chatInputHelpers.ts +0 -62
- package/src/chat/input/chatInputState.ts +0 -247
- package/src/chat/input/chatPaste.ts +0 -49
- package/src/chat/input/imageRefs.ts +0 -30
- package/src/chat/input/inputRendering.tsx +0 -93
- package/src/chat/input/textCursor.ts +0 -212
- package/src/chat/messageMarkdown.ts +0 -220
- package/src/chat/messageRows.ts +0 -43
- package/src/chat/planImplementation.ts +0 -62
- package/src/chat/slashCommandHandlers.ts +0 -122
- package/src/chat/slashCommandViews.ts +0 -120
- package/src/chat/transcript/TranscriptView.tsx +0 -184
- package/src/chat/transcript/transcriptViewport.ts +0 -295
- package/src/chat/views/ContextLimitView.tsx +0 -95
- package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
- package/src/chat/views/CopyPicker.tsx +0 -50
- package/src/chat/views/PermissionPrompt.tsx +0 -156
- package/src/chat/views/PermissionsView.tsx +0 -165
- package/src/chat/views/PlanApprovalView.tsx +0 -91
- package/src/chat/views/ResumeView.tsx +0 -273
- package/src/chat/views/RewindView.tsx +0 -412
- package/src/cli/preview.tsx +0 -14
- package/src/cli/updateNotice.ts +0 -54
- package/src/identity/continuity/privateEdit/apply.ts +0 -170
- package/src/identity/continuity/privateEdit/diff.ts +0 -6
- package/src/identity/continuity/privateEdit/files.ts +0 -23
- package/src/identity/continuity/privateEdit/types.ts +0 -28
- package/src/identity/continuity/privateEdit.ts +0 -46
- package/src/identity/hub/IdentityHub.tsx +0 -14
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
- package/src/identity/hub/ens/effects.ts +0 -218
- package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
- package/src/identity/hub/shared/effects/types.ts +0 -53
- package/src/identity/hub/shared/reconciliation/index.ts +0 -14
- package/src/identity/wallet/page/grainient.ts +0 -278
- package/src/identity/wallet/page/html.ts +0 -28
- package/src/identity/wallet/page/styles/base.ts +0 -260
- package/src/identity/wallet/page/styles/components.ts +0 -262
- package/src/identity/wallet/page/styles/index.ts +0 -5
- package/src/identity/wallet/page/styles/responsive.ts +0 -247
- package/src/identity/wallet/page.tsx +0 -38
- package/src/mcp/approvals.ts +0 -113
- package/src/mcp/config.ts +0 -235
- package/src/mcp/manager.ts +0 -482
- package/src/mcp/managerHelpers.ts +0 -70
- package/src/mcp/names.ts +0 -19
- package/src/mcp/output.ts +0 -96
- package/src/models/ModelPicker.tsx +0 -1009
- package/src/models/catalog.ts +0 -327
- package/src/models/huggingface.ts +0 -712
- package/src/models/huggingfaceStorage.ts +0 -136
- package/src/models/llamacpp.ts +0 -848
- package/src/models/llamacppCommands.ts +0 -44
- package/src/models/llamacppConfig.ts +0 -34
- package/src/models/llamacppDiscovery.ts +0 -176
- package/src/models/llamacppOutput.ts +0 -65
- package/src/models/llamacppPreflight.ts +0 -158
- package/src/models/modelDisplay.ts +0 -180
- package/src/models/modelPickerCatalogFlow.ts +0 -56
- package/src/models/modelPickerCredentials.ts +0 -166
- package/src/models/modelPickerData.ts +0 -41
- package/src/models/modelPickerDisplay.tsx +0 -132
- package/src/models/modelPickerHfFlow.ts +0 -192
- package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
- package/src/models/modelPickerOptions.ts +0 -457
- package/src/models/modelPickerTypes.ts +0 -69
- package/src/models/modelPickerUninstallFlow.ts +0 -48
- package/src/models/modelPickerViewHelpers.ts +0 -174
- package/src/models/modelRecommendation.ts +0 -139
- package/src/models/providerDisplay.ts +0 -16
- package/src/models/runtimeDetection.ts +0 -81
- package/src/models/uncensoredCatalog.ts +0 -86
- package/src/providers/anthropic.ts +0 -290
- package/src/providers/contracts.ts +0 -71
- package/src/providers/errors.ts +0 -80
- package/src/providers/gemini.ts +0 -391
- package/src/providers/openai-chat.ts +0 -474
- package/src/providers/openai-responses-format.ts +0 -177
- package/src/providers/openai-responses.ts +0 -306
- package/src/providers/openaiChatWire.ts +0 -124
- package/src/providers/registry.ts +0 -120
- package/src/providers/retry.ts +0 -58
- package/src/providers/sse.ts +0 -93
- package/src/runtime/compaction.ts +0 -395
- package/src/runtime/cwd.ts +0 -43
- package/src/runtime/providerTurn.ts +0 -38
- package/src/runtime/sessionMode.ts +0 -55
- package/src/runtime/systemPrompt.ts +0 -213
- package/src/runtime/textToolParser.ts +0 -161
- package/src/runtime/toolClaimGuards.ts +0 -143
- package/src/runtime/toolExecution.ts +0 -304
- package/src/runtime/toolIntent.ts +0 -143
- package/src/runtime/turn.ts +0 -369
- package/src/runtime/turnNudges.ts +0 -223
- package/src/runtime/turnTypes.ts +0 -86
- package/src/storage/factoryReset.ts +0 -127
- package/src/storage/history.ts +0 -58
- package/src/storage/permissions.ts +0 -76
- package/src/storage/rewind.ts +0 -266
- package/src/storage/sessionExport.ts +0 -49
- package/src/storage/sessions.ts +0 -495
- package/src/tools/bashSafety.ts +0 -186
- package/src/tools/bashTool.ts +0 -140
- package/src/tools/changeDirectoryTool.ts +0 -213
- package/src/tools/contracts.ts +0 -192
- package/src/tools/deleteFileTool.ts +0 -116
- package/src/tools/editTool.ts +0 -165
- package/src/tools/editUtils.ts +0 -170
- package/src/tools/fileDiff.ts +0 -261
- package/src/tools/listDirectoryTool.ts +0 -55
- package/src/tools/listSkillFilesTool.ts +0 -77
- package/src/tools/listSkillsTool.ts +0 -68
- package/src/tools/mcpResourceTools.ts +0 -95
- package/src/tools/permissionRules.ts +0 -85
- package/src/tools/privateContinuityEditTool.ts +0 -187
- package/src/tools/privateContinuityReadTool.ts +0 -106
- package/src/tools/readSkillTool.ts +0 -107
- package/src/tools/readTool.ts +0 -85
- package/src/tools/registry.ts +0 -103
- package/src/tools/writeFileTool.ts +0 -167
- package/src/ui/BrandSplash.tsx +0 -133
- package/src/ui/terminalTitle.ts +0 -30
- package/src/utils/images.ts +0 -140
- package/src/utils/markdownSegments.ts +0 -51
- package/src/utils/messages.ts +0 -37
- package/src/utils/withRetry.ts +0 -324
- /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
- /package/src/identity/{hub → manager}/custody/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
- /package/src/identity/{hub → manager}/ens/state.ts +0 -0
- /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
- /package/src/identity/{hub → manager}/ens/types.ts +0 -0
- /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
- /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
- /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
- /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
- /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
- /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
- /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
- /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
- /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
- /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
- /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
|
+
}
|
package/src/cli/sync.ts
ADDED
|
@@ -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
|
+
}
|