ethagent 1.1.1 → 2.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 (271) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +127 -29
  3. package/package.json +16 -9
  4. package/src/app/FirstRun.tsx +192 -146
  5. package/src/app/FirstRunTimeline.tsx +47 -0
  6. package/src/app/input/AppInputProvider.tsx +1 -1
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -1
  8. package/src/chat/ChatBottomPane.tsx +0 -1
  9. package/src/chat/ChatInput.tsx +6 -6
  10. package/src/chat/ChatScreen.tsx +43 -18
  11. package/src/chat/ContextLimitView.tsx +4 -4
  12. package/src/chat/ContinuityEditReviewView.tsx +11 -17
  13. package/src/chat/ConversationStack.tsx +3 -0
  14. package/src/chat/CopyPicker.tsx +0 -1
  15. package/src/chat/MessageList.tsx +62 -45
  16. package/src/chat/PermissionPrompt.tsx +13 -9
  17. package/src/chat/PlanApprovalView.tsx +3 -3
  18. package/src/chat/ResumeView.tsx +1 -4
  19. package/src/chat/RewindView.tsx +2 -2
  20. package/src/chat/TranscriptView.tsx +6 -0
  21. package/src/chat/chatInputState.ts +1 -1
  22. package/src/chat/chatScreenUtils.ts +22 -11
  23. package/src/chat/chatSessionState.ts +2 -2
  24. package/src/chat/chatTurnOrchestrator.ts +16 -81
  25. package/src/chat/commands.ts +1 -1
  26. package/src/chat/textCursor.ts +1 -1
  27. package/src/chat/transcriptViewport.ts +2 -7
  28. package/src/cli/ResetConfirmView.tsx +1 -1
  29. package/src/cli/main.tsx +9 -3
  30. package/src/cli/preview.tsx +0 -5
  31. package/src/cli/updateNotice.ts +5 -3
  32. package/src/identity/continuity/editor.ts +7 -107
  33. package/src/identity/continuity/envelope.ts +1048 -40
  34. package/src/identity/continuity/history.ts +4 -4
  35. package/src/identity/continuity/localBackup.ts +249 -0
  36. package/src/identity/continuity/privateEdit/apply.ts +170 -0
  37. package/src/identity/continuity/privateEdit/diff.ts +82 -0
  38. package/src/identity/continuity/privateEdit/files.ts +23 -0
  39. package/src/identity/continuity/privateEdit/types.ts +28 -0
  40. package/src/identity/continuity/privateEdit.ts +10 -298
  41. package/src/identity/continuity/publicSkills.ts +8 -9
  42. package/src/identity/continuity/snapshots.ts +17 -6
  43. package/src/identity/continuity/storage/defaults.ts +111 -0
  44. package/src/identity/continuity/storage/files.ts +72 -0
  45. package/src/identity/continuity/storage/markdown.ts +81 -0
  46. package/src/identity/continuity/storage/paths.ts +24 -0
  47. package/src/identity/continuity/storage/scaffold.ts +124 -0
  48. package/src/identity/continuity/storage/status.ts +86 -0
  49. package/src/identity/continuity/storage/types.ts +27 -0
  50. package/src/identity/continuity/storage.ts +32 -507
  51. package/src/identity/continuity/zipWriter.ts +95 -0
  52. package/src/identity/crypto/backupEnvelope.ts +14 -247
  53. package/src/identity/crypto/eth.ts +7 -7
  54. package/src/identity/ens/agentRecords.ts +96 -0
  55. package/src/identity/ens/ensAutomation/contracts.ts +38 -0
  56. package/src/identity/ens/ensAutomation/delete.ts +80 -0
  57. package/src/identity/ens/ensAutomation/names.ts +14 -0
  58. package/src/identity/ens/ensAutomation/operators.ts +29 -0
  59. package/src/identity/ens/ensAutomation/read.ts +114 -0
  60. package/src/identity/ens/ensAutomation/root.ts +63 -0
  61. package/src/identity/ens/ensAutomation/setup.ts +284 -0
  62. package/src/identity/ens/ensAutomation/transactions.ts +107 -0
  63. package/src/identity/ens/ensAutomation/types.ts +126 -0
  64. package/src/identity/ens/ensAutomation.ts +29 -0
  65. package/src/identity/ens/ensLookup/client.ts +43 -0
  66. package/src/identity/ens/ensLookup/constants.ts +26 -0
  67. package/src/identity/ens/ensLookup/discovery.ts +70 -0
  68. package/src/identity/ens/ensLookup/names.ts +34 -0
  69. package/src/identity/ens/ensLookup/records.ts +45 -0
  70. package/src/identity/ens/ensLookup/resolve.ts +75 -0
  71. package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
  72. package/src/identity/ens/ensLookup/types.ts +38 -0
  73. package/src/identity/ens/ensLookup/validation.ts +72 -0
  74. package/src/identity/ens/ensLookup.ts +19 -0
  75. package/src/identity/ens/ensRegistration.ts +199 -0
  76. package/src/identity/ens/resolverDelegation.ts +48 -0
  77. package/src/identity/hub/IdentityHub.tsx +13 -815
  78. package/src/identity/hub/OperationalRoutes.tsx +370 -0
  79. package/src/identity/hub/Routes.tsx +361 -0
  80. package/src/identity/hub/advancedEnsValidation.ts +45 -0
  81. package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
  82. package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
  83. package/src/identity/hub/components/FlowTimeline.tsx +27 -0
  84. package/src/identity/hub/components/IdentitySummary.tsx +190 -0
  85. package/src/identity/hub/components/MenuScreen.tsx +237 -0
  86. package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
  87. package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
  88. package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
  89. package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
  90. package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
  91. package/src/identity/hub/effects/create.ts +310 -0
  92. package/src/identity/hub/effects/ens/flows.ts +218 -0
  93. package/src/identity/hub/effects/ens/index.ts +11 -0
  94. package/src/identity/hub/effects/ens/transactions.ts +239 -0
  95. package/src/identity/hub/effects/index.ts +74 -0
  96. package/src/identity/hub/effects/profile/profileState.ts +173 -0
  97. package/src/identity/hub/effects/publicProfile/index.ts +5 -0
  98. package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
  99. package/src/identity/hub/effects/rebackup/index.ts +7 -0
  100. package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
  101. package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
  102. package/src/identity/hub/effects/receipts.ts +46 -0
  103. package/src/identity/hub/effects/restore/apply.ts +112 -0
  104. package/src/identity/hub/effects/restore/auth.ts +159 -0
  105. package/src/identity/hub/effects/restore/discover.ts +86 -0
  106. package/src/identity/hub/effects/restore/envelopes.ts +21 -0
  107. package/src/identity/hub/effects/restore/fetch.ts +25 -0
  108. package/src/identity/hub/effects/restore/index.ts +22 -0
  109. package/src/identity/hub/effects/restore/recovery.ts +135 -0
  110. package/src/identity/hub/effects/restore/resolve.ts +102 -0
  111. package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
  112. package/src/identity/hub/effects/restore/shared.ts +91 -0
  113. package/src/identity/hub/effects/restoreAdmin.ts +93 -0
  114. package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
  115. package/src/identity/hub/effects/shared/snapshot.ts +336 -0
  116. package/src/identity/hub/effects/shared/sync.ts +190 -0
  117. package/src/identity/hub/effects/token-transfer/index.ts +6 -0
  118. package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
  119. package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
  120. package/src/identity/hub/effects/types.ts +53 -0
  121. package/src/identity/hub/effects/vault/preflight.ts +50 -0
  122. package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
  123. package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
  124. package/src/identity/hub/flows/continuity/RecoveryConfirmScreen.tsx +104 -0
  125. package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
  126. package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
  127. package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
  128. package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
  129. package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
  130. package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
  131. package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
  132. package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
  133. package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
  134. package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
  135. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
  136. package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
  137. package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
  138. package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
  139. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
  140. package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
  141. package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
  142. package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
  143. package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
  144. package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
  145. package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
  146. package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
  147. package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
  148. package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
  149. package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +25 -43
  150. package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
  151. package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
  152. package/src/identity/hub/identityHubReducer.ts +166 -101
  153. package/src/identity/hub/model/continuity.ts +94 -0
  154. package/src/identity/hub/model/copy.ts +35 -0
  155. package/src/identity/hub/model/custody.ts +54 -0
  156. package/src/identity/hub/model/ens.ts +49 -0
  157. package/src/identity/hub/model/errors.ts +140 -0
  158. package/src/identity/hub/model/format.ts +15 -0
  159. package/src/identity/hub/model/identity.ts +94 -0
  160. package/src/identity/hub/model/network.ts +32 -0
  161. package/src/identity/hub/model/transfer.ts +57 -0
  162. package/src/identity/hub/operatorWallets.ts +131 -0
  163. package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
  164. package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
  165. package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
  166. package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
  167. package/src/identity/hub/reconciliation/index.ts +21 -0
  168. package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
  169. package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
  170. package/src/identity/hub/txGuard.ts +51 -0
  171. package/src/identity/hub/types.ts +17 -0
  172. package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
  173. package/src/identity/hub/useIdentityHubController.ts +396 -0
  174. package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
  175. package/src/identity/hub/utils.ts +79 -0
  176. package/src/identity/identityCompat.ts +34 -0
  177. package/src/identity/profile/agentIcon.ts +61 -0
  178. package/src/identity/profile/imagePicker.ts +12 -12
  179. package/src/identity/registry/erc8004/abi.ts +14 -0
  180. package/src/identity/registry/erc8004/chains.ts +150 -0
  181. package/src/identity/registry/erc8004/client.ts +11 -0
  182. package/src/identity/registry/erc8004/discovery.ts +511 -0
  183. package/src/identity/registry/erc8004/metadata.ts +335 -0
  184. package/src/identity/registry/erc8004/ownership.ts +121 -0
  185. package/src/identity/registry/erc8004/preflight.ts +123 -0
  186. package/src/identity/registry/erc8004/transactions.ts +77 -0
  187. package/src/identity/registry/erc8004/types.ts +88 -0
  188. package/src/identity/registry/erc8004/uri.ts +59 -0
  189. package/src/identity/registry/erc8004/utils.ts +58 -0
  190. package/src/identity/registry/erc8004.ts +53 -1106
  191. package/src/identity/registry/fieldParsers.ts +28 -0
  192. package/src/identity/registry/operatorVault/bytecode.ts +98 -0
  193. package/src/identity/registry/operatorVault/constants.ts +38 -0
  194. package/src/identity/registry/operatorVault/read.ts +246 -0
  195. package/src/identity/registry/operatorVault/transactions.ts +81 -0
  196. package/src/identity/registry/operatorVault.ts +44 -0
  197. package/src/identity/storage/ipfs.ts +26 -24
  198. package/src/identity/wallet/browserWallet/gas.ts +41 -0
  199. package/src/identity/wallet/browserWallet/html.ts +106 -0
  200. package/src/identity/wallet/browserWallet/http.ts +28 -0
  201. package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
  202. package/src/identity/wallet/browserWallet/requests.ts +191 -0
  203. package/src/identity/wallet/browserWallet/session.ts +325 -0
  204. package/src/identity/wallet/browserWallet/types.ts +192 -0
  205. package/src/identity/wallet/browserWallet/validation.ts +74 -0
  206. package/src/identity/wallet/browserWallet.ts +30 -393
  207. package/src/identity/wallet/page/constants.ts +5 -0
  208. package/src/identity/wallet/page/controller.ts +251 -0
  209. package/src/identity/wallet/page/copy.ts +340 -0
  210. package/src/identity/wallet/page/grainient.ts +278 -0
  211. package/src/identity/wallet/page/html.ts +28 -0
  212. package/src/identity/wallet/page/markup.ts +50 -0
  213. package/src/identity/wallet/page/state.ts +9 -0
  214. package/src/identity/wallet/page/styles/base.ts +259 -0
  215. package/src/identity/wallet/page/styles/components.ts +262 -0
  216. package/src/identity/wallet/page/styles/index.ts +5 -0
  217. package/src/identity/wallet/page/styles/responsive.ts +247 -0
  218. package/src/identity/wallet/page/types.ts +47 -0
  219. package/src/identity/wallet/page/view.ts +535 -0
  220. package/src/identity/wallet/page/walletProvider.ts +70 -0
  221. package/src/identity/wallet/page.tsx +38 -0
  222. package/src/identity/wallet/walletPurposeCompat.ts +27 -0
  223. package/src/mcp/manager.ts +0 -1
  224. package/src/models/ModelPicker.tsx +36 -30
  225. package/src/models/catalog.ts +5 -2
  226. package/src/models/huggingface.ts +9 -9
  227. package/src/models/llamacpp.ts +13 -13
  228. package/src/models/modelDisplay.ts +75 -0
  229. package/src/models/modelPickerOptions.ts +16 -3
  230. package/src/models/modelRecommendation.ts +0 -1
  231. package/src/providers/errors.ts +16 -0
  232. package/src/providers/gemini.ts +252 -39
  233. package/src/providers/registry.ts +2 -2
  234. package/src/providers/retry.ts +1 -1
  235. package/src/runtime/sessionMode.ts +1 -1
  236. package/src/runtime/systemPrompt.ts +2 -0
  237. package/src/runtime/toolExecution.ts +18 -22
  238. package/src/runtime/toolIntent.ts +0 -20
  239. package/src/runtime/turn.ts +0 -92
  240. package/src/storage/atomicWrite.ts +4 -1
  241. package/src/storage/config.ts +181 -5
  242. package/src/storage/identity.ts +9 -3
  243. package/src/storage/secrets.ts +2 -2
  244. package/src/tools/bashSafety.ts +8 -0
  245. package/src/tools/changeDirectoryTool.ts +1 -1
  246. package/src/tools/deleteFileTool.ts +4 -4
  247. package/src/tools/editTool.ts +4 -4
  248. package/src/tools/editUtils.ts +5 -5
  249. package/src/tools/privateContinuityEditTool.ts +4 -5
  250. package/src/tools/privateContinuityReadTool.ts +1 -2
  251. package/src/tools/registry.ts +30 -0
  252. package/src/tools/writeFileTool.ts +5 -5
  253. package/src/ui/BrandSplash.tsx +20 -85
  254. package/src/ui/ProgressBar.tsx +3 -5
  255. package/src/ui/Select.tsx +21 -9
  256. package/src/ui/Spinner.tsx +38 -3
  257. package/src/ui/Surface.tsx +3 -3
  258. package/src/ui/TextInput.tsx +191 -29
  259. package/src/ui/theme.ts +7 -34
  260. package/src/utils/openExternal.ts +21 -0
  261. package/src/utils/withRetry.ts +47 -3
  262. package/src/identity/hub/identityHubEffects.ts +0 -937
  263. package/src/identity/hub/identityHubModel.ts +0 -291
  264. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -144
  265. package/src/identity/hub/screens/EditProfileFlow.tsx +0 -145
  266. package/src/identity/hub/screens/IdentitySummary.tsx +0 -90
  267. package/src/identity/hub/screens/MenuScreen.tsx +0 -117
  268. package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +0 -87
  269. package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
  270. package/src/identity/wallet/wallet-page/wallet.html +0 -1202
  271. /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
@@ -11,7 +11,7 @@ import {
11
11
  type PrivateContinuityFile,
12
12
  } from './storage.js'
13
13
 
14
- export type PrivateContinuityHistorySnapshot = {
14
+ type PrivateContinuityHistorySnapshot = {
15
15
  version: 1
16
16
  id: string
17
17
  createdAt: string
@@ -35,7 +35,7 @@ export type PrivateContinuityHistorySnapshot = {
35
35
  checkpointLabel?: string
36
36
  }
37
37
 
38
- export type RecordPrivateContinuityHistoryInput = {
38
+ type RecordPrivateContinuityHistoryInput = {
39
39
  identity: EthagentIdentity
40
40
  file: PrivateContinuityFile
41
41
  filePath: string
@@ -51,7 +51,7 @@ export type RecordPrivateContinuityHistoryInput = {
51
51
  checkpointLabel?: string
52
52
  }
53
53
 
54
- export function privateContinuityHistoryPath(identity: EthagentIdentity): string {
54
+ function privateContinuityHistoryPath(identity: EthagentIdentity): string {
55
55
  return path.join(continuityVaultRef(identity).dir, '.history.jsonl')
56
56
  }
57
57
 
@@ -121,7 +121,7 @@ export async function restorePrivateContinuityHistorySnapshot(
121
121
  ): Promise<PrivateContinuityHistorySnapshot> {
122
122
  const snapshot = (await listPrivateContinuityHistory(identity, 500))
123
123
  .find(item => item.id === snapshotId)
124
- if (!snapshot) throw new Error('private continuity checkpoint was not found')
124
+ if (!snapshot) throw new Error('Private continuity checkpoint was not found')
125
125
 
126
126
  if (snapshot.previousFiles) {
127
127
  await writeContinuityFiles(identity, snapshot.previousFiles)
@@ -0,0 +1,249 @@
1
+ import { spawn } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import fsp from 'node:fs/promises'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
6
+ import type { EthagentIdentity } from '../../storage/config.js'
7
+ import { continuityVaultRef } from './storage.js'
8
+ import { buildZip, type ZipEntry } from './zipWriter.js'
9
+
10
+ type LocalBackupResult =
11
+ | { ok: true; path: string; method: string }
12
+ | { ok: false; cancelled: boolean; error: string }
13
+
14
+ type SaveDialogCommand = {
15
+ cmd: string
16
+ args: string[]
17
+ method: string
18
+ }
19
+
20
+ export async function exportLocalBackup(
21
+ identity: EthagentIdentity,
22
+ options: {
23
+ platform?: NodeJS.Platform
24
+ env?: NodeJS.ProcessEnv
25
+ timeoutMs?: number
26
+ spawnImpl?: typeof spawn
27
+ homeDir?: string
28
+ } = {},
29
+ ): Promise<LocalBackupResult> {
30
+ const platform = options.platform ?? process.platform
31
+ const env = options.env ?? process.env
32
+ const homeDir = options.homeDir ?? os.homedir()
33
+ const ref = continuityVaultRef(identity)
34
+
35
+ const entries: ZipEntry[] = []
36
+ for (const [name, file] of [
37
+ ['SOUL.md', ref.soulPath],
38
+ ['MEMORY.md', ref.memoryPath],
39
+ ['skills.json', ref.publicSkillsPath],
40
+ ] as const) {
41
+ try {
42
+ const data = await fsp.readFile(file)
43
+ entries.push({ name, data })
44
+ } catch (err: unknown) {
45
+ const code = (err as NodeJS.ErrnoException).code
46
+ if (code !== 'ENOENT') {
47
+ return { ok: false, cancelled: false, error: `read ${name} failed: ${(err as Error).message}` }
48
+ }
49
+ }
50
+ }
51
+ if (entries.length === 0) {
52
+ return { ok: false, cancelled: false, error: 'no local continuity files to back up' }
53
+ }
54
+
55
+ const archive = buildZip(entries)
56
+ const defaultName = defaultBackupFilename(identity)
57
+ const dialog = resolveSaveDialogCommand(platform, env, defaultName)
58
+
59
+ if (dialog) {
60
+ const chosen = await runSaveDialog(dialog, options.spawnImpl ?? spawn, options.timeoutMs ?? 120_000)
61
+ if (chosen.ok) {
62
+ const target = ensureZipExtension(chosen.file)
63
+ try {
64
+ await fsp.writeFile(target, archive)
65
+ return { ok: true, path: target, method: dialog.method }
66
+ } catch (err: unknown) {
67
+ return { ok: false, cancelled: false, error: `write failed: ${(err as Error).message}` }
68
+ }
69
+ }
70
+ if (!chosen.cancelled) {
71
+ const fallback = path.join(homeDir, defaultName)
72
+ try {
73
+ await fsp.writeFile(fallback, archive)
74
+ return { ok: true, path: fallback, method: 'home dir fallback' }
75
+ } catch (err: unknown) {
76
+ return { ok: false, cancelled: false, error: `write failed: ${(err as Error).message}` }
77
+ }
78
+ }
79
+ return chosen
80
+ }
81
+
82
+ const fallback = path.join(homeDir, defaultName)
83
+ try {
84
+ await fsp.writeFile(fallback, archive)
85
+ return { ok: true, path: fallback, method: 'home dir' }
86
+ } catch (err: unknown) {
87
+ return { ok: false, cancelled: false, error: `write failed: ${(err as Error).message}` }
88
+ }
89
+ }
90
+
91
+ function defaultBackupFilename(identity: EthagentIdentity): string {
92
+ const idPart = identity.agentId ? `agent-${identity.agentId}` : 'agent'
93
+ const stamp = timestampSlug(new Date())
94
+ return `ethagent-backup-${idPart}-${stamp}.zip`
95
+ }
96
+
97
+ function timestampSlug(date: Date): string {
98
+ const pad = (n: number): string => String(n).padStart(2, '0')
99
+ return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
100
+ }
101
+
102
+ function ensureZipExtension(file: string): string {
103
+ return /\.zip$/i.test(file) ? file : `${file}.zip`
104
+ }
105
+
106
+ function resolveSaveDialogCommand(platform: NodeJS.Platform, env: NodeJS.ProcessEnv, defaultName: string): SaveDialogCommand | null {
107
+ if (platform === 'win32') {
108
+ const powershell = findExecutable('powershell.exe', env, platform) ?? findExecutable('pwsh.exe', env, platform)
109
+ if (!powershell) return null
110
+ return {
111
+ cmd: powershell,
112
+ args: [
113
+ '-NoProfile',
114
+ '-STA',
115
+ '-ExecutionPolicy',
116
+ 'Bypass',
117
+ '-Command',
118
+ [
119
+ '[Console]::OutputEncoding = [System.Text.Encoding]::UTF8',
120
+ 'Add-Type -AssemblyName System.Windows.Forms',
121
+ '$dialog = New-Object System.Windows.Forms.SaveFileDialog',
122
+ '$dialog.Title = "Save Local Backup"',
123
+ '$dialog.Filter = "Zip archive (*.zip)|*.zip|All files (*.*)|*.*"',
124
+ '$dialog.DefaultExt = "zip"',
125
+ `$dialog.FileName = "${defaultName.replace(/"/g, '`"')}"`,
126
+ '$dialog.OverwritePrompt = $true',
127
+ 'if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { Write-Output $dialog.FileName }',
128
+ ].join('; '),
129
+ ],
130
+ method: 'windows save dialog',
131
+ }
132
+ }
133
+ if (platform === 'darwin') {
134
+ return {
135
+ cmd: 'osascript',
136
+ args: [
137
+ '-e',
138
+ `set chosen to choose file name with prompt "Save Local Backup" default name "${defaultName.replace(/"/g, '\\"')}"`,
139
+ '-e',
140
+ 'POSIX path of chosen',
141
+ ],
142
+ method: 'macOS save dialog',
143
+ }
144
+ }
145
+ const zenity = findExecutable('zenity', env, platform)
146
+ if (zenity) {
147
+ return {
148
+ cmd: zenity,
149
+ args: [
150
+ '--file-selection',
151
+ '--save',
152
+ '--confirm-overwrite',
153
+ '--title=Save Local Backup',
154
+ `--filename=${path.join(env.HOME ?? '.', defaultName)}`,
155
+ '--file-filter=Zip archive | *.zip',
156
+ ],
157
+ method: 'zenity',
158
+ }
159
+ }
160
+ const kdialog = findExecutable('kdialog', env, platform)
161
+ if (kdialog) {
162
+ return {
163
+ cmd: kdialog,
164
+ args: ['--getsavefilename', path.join(env.HOME ?? '.', defaultName), 'Zip archive (*.zip)'],
165
+ method: 'kdialog',
166
+ }
167
+ }
168
+ return null
169
+ }
170
+
171
+ function runSaveDialog(
172
+ command: SaveDialogCommand,
173
+ spawnImpl: typeof spawn,
174
+ timeoutMs: number,
175
+ ): Promise<{ ok: true; file: string } | { ok: false; cancelled: boolean; error: string }> {
176
+ return new Promise(resolve => {
177
+ let stdout = ''
178
+ let stderr = ''
179
+ let settled = false
180
+ let child: ReturnType<typeof spawn>
181
+ try {
182
+ child = spawnImpl(command.cmd, command.args, {
183
+ stdio: ['ignore', 'pipe', 'pipe'],
184
+ windowsHide: true,
185
+ })
186
+ } catch (err: unknown) {
187
+ resolve({ ok: false, cancelled: false, error: (err as Error).message })
188
+ return
189
+ }
190
+ const timer = setTimeout(() => {
191
+ if (settled) return
192
+ settled = true
193
+ child.kill()
194
+ resolve({ ok: false, cancelled: false, error: 'save dialog timed out' })
195
+ }, timeoutMs)
196
+ child.stdout?.setEncoding('utf8')
197
+ child.stderr?.setEncoding('utf8')
198
+ child.stdout?.on('data', chunk => { stdout += String(chunk) })
199
+ child.stderr?.on('data', chunk => { stderr += String(chunk) })
200
+ child.on('error', err => {
201
+ if (settled) return
202
+ settled = true
203
+ clearTimeout(timer)
204
+ resolve({ ok: false, cancelled: false, error: err.message })
205
+ })
206
+ child.on('close', code => {
207
+ if (settled) return
208
+ settled = true
209
+ clearTimeout(timer)
210
+ const file = stdout.trim()
211
+ if (code === 0 && file) {
212
+ resolve({ ok: true, file })
213
+ return
214
+ }
215
+ const detail = stderr.trim()
216
+ const cancelled = code === 0 || /cancel/i.test(detail)
217
+ resolve({ ok: false, cancelled, error: cancelled ? 'save cancelled' : detail || `${command.method} exited ${code}` })
218
+ })
219
+ })
220
+ }
221
+
222
+ function findExecutable(command: string, env: NodeJS.ProcessEnv, platform: NodeJS.Platform): string | null {
223
+ const hasPathSeparator = command.includes('/') || command.includes('\\')
224
+ if (hasPathSeparator || path.isAbsolute(command)) return canAccessExecutable(command) ? command : null
225
+ const pathParts = (env.PATH ?? '').split(path.delimiter).filter(Boolean)
226
+ const extensions = platform === 'win32'
227
+ ? (env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
228
+ : ['']
229
+ for (const dir of pathParts) {
230
+ for (const ext of extensions) {
231
+ const candidate = path.join(dir, platform === 'win32' && path.extname(command) === '' ? `${command}${ext}` : command)
232
+ if (canAccessExecutable(candidate)) return candidate
233
+ if (platform === 'win32') {
234
+ const lower = path.join(dir, path.extname(command) === '' ? `${command}${ext.toLowerCase()}` : command)
235
+ if (canAccessExecutable(lower)) return lower
236
+ }
237
+ }
238
+ }
239
+ return null
240
+ }
241
+
242
+ function canAccessExecutable(file: string): boolean {
243
+ try {
244
+ fs.accessSync(file, fs.constants.X_OK)
245
+ return true
246
+ } catch {
247
+ return false
248
+ }
249
+ }
@@ -0,0 +1,170 @@
1
+ import type { EthagentIdentity } from '../../../storage/config.js'
2
+ import { applyRequestedEdit } from '../../../tools/editUtils.js'
3
+ import { defaultContinuityFiles, type PrivateContinuityFile } from '../storage.js'
4
+ import type { PrivateContinuityEditInput } from './types.js'
5
+
6
+ export function applyPrivateContinuityEdit(input: PrivateContinuityEditInput, before: string, identity: EthagentIdentity) {
7
+ if (input.replaceWholeFile) {
8
+ throw new Error('Private continuity files must be edited in place; whole-file replacement is disabled')
9
+ }
10
+ if (input.appendToSection || input.appendText) {
11
+ if (!input.appendToSection?.trim()) throw new Error('Field appendToSection is required for append edits')
12
+ if (!input.appendText?.trim()) throw new Error('Field appendText is required for append edits')
13
+ if (input.oldText || input.newText !== undefined) {
14
+ throw new Error('Use either appendToSection+appendText or oldText+newText, not both')
15
+ }
16
+ return appendToMarkdownSection(identity, input.file, before, input.appendToSection, input.appendText)
17
+ }
18
+ if (!input.oldText?.trim()) {
19
+ throw new Error('Field oldText is required; private continuity edits must patch existing scaffold text')
20
+ }
21
+ if (input.newText === undefined) {
22
+ throw new Error('Field newText is required for targeted private continuity edits')
23
+ }
24
+ return applyRequestedEdit(
25
+ input.file,
26
+ before,
27
+ input.oldText,
28
+ input.newText,
29
+ input.replaceAll ?? false,
30
+ false,
31
+ )
32
+ }
33
+
34
+ function appendToMarkdownSection(
35
+ identity: EthagentIdentity,
36
+ file: PrivateContinuityFile,
37
+ before: string,
38
+ section: string,
39
+ appendText: string,
40
+ ) {
41
+ const heading = normalizeSectionHeading(section)
42
+ let working = before
43
+ let repairedMissingSection = false
44
+ let lines = working.split(/\r?\n/)
45
+ let bounds = findMarkdownSectionBounds(lines, heading)
46
+ if (!bounds) {
47
+ const repaired = insertDefaultScaffoldSection(identity, file, before, heading)
48
+ if (!repaired) {
49
+ throw new Error(`section "${section}" was not found in ${file}; target an existing scaffold section`)
50
+ }
51
+ working = repaired
52
+ repairedMissingSection = true
53
+ lines = working.split(/\r?\n/)
54
+ bounds = findMarkdownSectionBounds(lines, heading)
55
+ }
56
+ if (!bounds) {
57
+ throw new Error(`section "${section}" was not found in ${file}; target an existing scaffold section`)
58
+ }
59
+ const { start, end: insertAt } = bounds
60
+ const prefix = lines.slice(0, insertAt).join('\n').replace(/\s+$/g, '')
61
+ const suffix = insertAt >= lines.length ? '' : lines.slice(insertAt).join('\n').replace(/^\s+/g, '')
62
+ const normalizedAppend = ensureTrailingNewline(appendText.trim())
63
+ const after = ensureTrailingNewline(suffix
64
+ ? `${prefix}\n${normalizedAppend}\n${suffix}`
65
+ : `${prefix}\n${normalizedAppend}`)
66
+ const afterLines = after.split(/\r?\n/)
67
+ const afterBounds = findMarkdownSectionBounds(afterLines, heading)
68
+ return {
69
+ before,
70
+ after,
71
+ summary: repairedMissingSection
72
+ ? `repair ${heading} section and append to ${heading} in ${file}`
73
+ : `append to ${heading} in ${file}`,
74
+ previewBefore: repairedMissingSection
75
+ ? `section "${heading}" was missing in ${file}; approval will add the scaffold section before appending.`
76
+ : previewText(sectionPreview(lines, start, insertAt)),
77
+ previewAfter: previewText(afterBounds
78
+ ? sectionPreview(afterLines, afterBounds.start, afterBounds.end)
79
+ : normalizedAppend),
80
+ }
81
+ }
82
+
83
+ function insertDefaultScaffoldSection(
84
+ identity: EthagentIdentity,
85
+ file: PrivateContinuityFile,
86
+ before: string,
87
+ heading: string,
88
+ ): string | null {
89
+ const defaults = defaultContinuityFiles(identity)[file]
90
+ const defaultSection = extractMarkdownSection(defaults, heading)
91
+ if (!defaultSection) return null
92
+
93
+ const defaultHeadings = markdownSectionHeadings(defaults)
94
+ const targetIndex = defaultHeadings.indexOf(heading)
95
+ if (targetIndex === -1) return null
96
+
97
+ const lines = before.split(/\r?\n/)
98
+ const followingHeadings = new Set(defaultHeadings.slice(targetIndex + 1))
99
+ const followingIndex = lines.findIndex(line => followingHeadings.has(normalizeSectionHeading(line)))
100
+ if (followingIndex !== -1) {
101
+ return insertSectionAtLine(before, followingIndex, defaultSection)
102
+ }
103
+
104
+ const previousHeadings = new Set(defaultHeadings.slice(0, targetIndex))
105
+ let insertAfterPrevious: number | null = null
106
+ for (let index = 0; index < lines.length; index += 1) {
107
+ if (!previousHeadings.has(normalizeSectionHeading(lines[index] ?? ''))) continue
108
+ const bounds = findMarkdownSectionBounds(lines, normalizeSectionHeading(lines[index] ?? ''))
109
+ if (bounds && bounds.end > (insertAfterPrevious ?? -1)) insertAfterPrevious = bounds.end
110
+ }
111
+ if (insertAfterPrevious !== null) {
112
+ return insertSectionAtLine(before, insertAfterPrevious, defaultSection)
113
+ }
114
+
115
+ const firstHeading = lines.findIndex(line => /^#\s+/.test(line.trim()))
116
+ return insertSectionAtLine(before, firstHeading === -1 ? 0 : firstHeading + 1, defaultSection)
117
+ }
118
+
119
+ function insertSectionAtLine(markdown: string, lineIndex: number, section: string): string {
120
+ const lines = markdown.split(/\r?\n/)
121
+ const before = lines.slice(0, lineIndex).join('\n').replace(/\s+$/g, '')
122
+ const after = lines.slice(lineIndex).join('\n').replace(/^\s+/g, '')
123
+ const block = section.trim()
124
+ if (!before) return ensureTrailingNewline(after ? `${block}\n\n${after}` : block)
125
+ return ensureTrailingNewline(after ? `${before}\n\n${block}\n\n${after}` : `${before}\n\n${block}`)
126
+ }
127
+
128
+ function extractMarkdownSection(markdown: string, heading: string): string | null {
129
+ const lines = markdown.split(/\r?\n/)
130
+ const bounds = findMarkdownSectionBounds(lines, heading)
131
+ if (!bounds) return null
132
+ return lines.slice(bounds.start, bounds.end).join('\n').trim()
133
+ }
134
+
135
+ function markdownSectionHeadings(markdown: string): string[] {
136
+ return markdown
137
+ .split(/\r?\n/)
138
+ .filter(line => /^##\s+/.test(line.trim()))
139
+ .map(normalizeSectionHeading)
140
+ }
141
+
142
+ function findMarkdownSectionBounds(lines: string[], heading: string): { start: number; end: number } | null {
143
+ const start = lines.findIndex(line => normalizeSectionHeading(line) === heading && /^#{1,6}\s+/.test(line.trim()))
144
+ if (start === -1) return null
145
+ const nextSection = lines.findIndex((line, index) => index > start && /^##\s+/.test(line.trim()))
146
+ return { start, end: nextSection === -1 ? lines.length : nextSection }
147
+ }
148
+
149
+ function normalizeSectionHeading(value: string): string {
150
+ return value.trim().replace(/^#+\s*/, '').trim()
151
+ }
152
+
153
+ function sectionPreview(lines: string[], start: number, end: number): string {
154
+ return lines.slice(start, Math.min(end, start + 8)).join('\n')
155
+ }
156
+
157
+ function markdownLines(value: string): string[] {
158
+ const lines = value.split(/\r?\n/)
159
+ if (lines[lines.length - 1] === '') lines.pop()
160
+ return lines
161
+ }
162
+
163
+ function ensureTrailingNewline(value: string): string {
164
+ return value.endsWith('\n') ? value : `${value}\n`
165
+ }
166
+
167
+ function previewText(text: string, max = 700): string {
168
+ if (text.length <= max) return text
169
+ return `${text.slice(0, max - 3)}...`
170
+ }
@@ -0,0 +1,82 @@
1
+ import type { PrivateContinuityFile } from '../storage.js'
2
+
3
+ export function renderPrivateContinuityDiff(file: PrivateContinuityFile, before: string, after: string): string {
4
+ if (before === after) return '(no changes)'
5
+ const changedLines = changedMarkdownLines(before, after)
6
+ const lines = [
7
+ `--- ${file}`,
8
+ `+++ ${file}`,
9
+ ...(changedLines.length > 0 ? changedLines : ['(only whitespace or line ending changes)']),
10
+ ]
11
+ const diff = lines.join('\n')
12
+ return diff.length <= 2400 ? diff : `${diff.slice(0, 2397)}...`
13
+ }
14
+
15
+ function changedMarkdownLines(before: string, after: string): string[] {
16
+ const beforeLines = markdownLines(before)
17
+ const afterLines = markdownLines(after)
18
+ const lengths = lcsLengths(beforeLines, afterLines)
19
+ const changed: string[] = []
20
+ let beforeIndex = 0
21
+ let afterIndex = 0
22
+
23
+ while (beforeIndex < beforeLines.length && afterIndex < afterLines.length) {
24
+ if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
25
+ beforeIndex += 1
26
+ afterIndex += 1
27
+ continue
28
+ }
29
+
30
+ const deleteScore = lengths[beforeIndex + 1]![afterIndex]!
31
+ const insertScore = lengths[beforeIndex]![afterIndex + 1]!
32
+ const deleteRevealsMatch = beforeLines[beforeIndex + 1] === afterLines[afterIndex]
33
+ const insertRevealsMatch = beforeLines[beforeIndex] === afterLines[afterIndex + 1]
34
+
35
+ if (insertRevealsMatch && insertScore >= deleteScore) {
36
+ changed.push(`+${afterLines[afterIndex]}`)
37
+ afterIndex += 1
38
+ } else if (deleteRevealsMatch && deleteScore >= insertScore) {
39
+ changed.push(`-${beforeLines[beforeIndex]}`)
40
+ beforeIndex += 1
41
+ } else if (deleteScore >= insertScore) {
42
+ changed.push(`-${beforeLines[beforeIndex]}`)
43
+ beforeIndex += 1
44
+ } else {
45
+ changed.push(`+${afterLines[afterIndex]}`)
46
+ afterIndex += 1
47
+ }
48
+ }
49
+
50
+ while (beforeIndex < beforeLines.length) {
51
+ changed.push(`-${beforeLines[beforeIndex]}`)
52
+ beforeIndex += 1
53
+ }
54
+ while (afterIndex < afterLines.length) {
55
+ changed.push(`+${afterLines[afterIndex]}`)
56
+ afterIndex += 1
57
+ }
58
+
59
+ return changed
60
+ }
61
+
62
+ function lcsLengths(beforeLines: string[], afterLines: string[]): number[][] {
63
+ const lengths = Array.from(
64
+ { length: beforeLines.length + 1 },
65
+ () => Array<number>(afterLines.length + 1).fill(0),
66
+ )
67
+
68
+ for (let beforeIndex = beforeLines.length - 1; beforeIndex >= 0; beforeIndex -= 1) {
69
+ for (let afterIndex = afterLines.length - 1; afterIndex >= 0; afterIndex -= 1) {
70
+ lengths[beforeIndex]![afterIndex] = beforeLines[beforeIndex] === afterLines[afterIndex]
71
+ ? lengths[beforeIndex + 1]![afterIndex + 1]! + 1
72
+ : Math.max(lengths[beforeIndex + 1]![afterIndex]!, lengths[beforeIndex]![afterIndex + 1]!)
73
+ }
74
+ }
75
+
76
+ return lengths
77
+ }
78
+
79
+ function markdownLines(value: string): string[] {
80
+ const normalized = value.replace(/\r\n?/g, '\n')
81
+ return normalized.split('\n')
82
+ }
@@ -0,0 +1,23 @@
1
+ import fs from 'node:fs/promises'
2
+ import { type EthagentIdentity } from '../../../storage/config.js'
3
+ import { continuityVaultRef, defaultContinuityFiles, type PrivateContinuityFile } from '../storage.js'
4
+
5
+ export function privateContinuityPath(identity: EthagentIdentity, file: PrivateContinuityFile): string {
6
+ const ref = continuityVaultRef(identity)
7
+ return file === 'SOUL.md' ? ref.soulPath : ref.memoryPath
8
+ }
9
+
10
+ export async function readPrivateContinuityFile(
11
+ identity: EthagentIdentity,
12
+ file: PrivateContinuityFile,
13
+ fullPath: string,
14
+ ): Promise<{ content: string; existedBefore: boolean }> {
15
+ try {
16
+ return { content: await fs.readFile(fullPath, 'utf8'), existedBefore: true }
17
+ } catch (err: unknown) {
18
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
19
+ return { content: defaultContinuityFiles(identity)[file], existedBefore: false }
20
+ }
21
+ throw err
22
+ }
23
+ }
@@ -0,0 +1,28 @@
1
+ import type { EthagentIdentity } from '../../../storage/config.js'
2
+ import type { PrivateContinuityFile } from '../storage.js'
3
+
4
+ export type PrivateContinuityEditInput = {
5
+ file: PrivateContinuityFile
6
+ oldText?: string
7
+ newText?: string
8
+ appendToSection?: string
9
+ appendText?: string
10
+ replaceAll?: boolean
11
+ replaceWholeFile?: boolean
12
+ }
13
+
14
+ export type PreparedPrivateContinuityEdit = {
15
+ identity: EthagentIdentity
16
+ file: PrivateContinuityFile
17
+ fullPath: string
18
+ relativePath: string
19
+ directoryPath: string
20
+ existedBefore: boolean
21
+ previousContent: string
22
+ before: string
23
+ after: string
24
+ previewBefore: string
25
+ previewAfter: string
26
+ changeSummary: string
27
+ diff: string
28
+ }