ethagent 2.4.0 → 3.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 (98) hide show
  1. package/README.md +7 -4
  2. package/package.json +2 -1
  3. package/src/app/FirstRun.tsx +155 -15
  4. package/src/app/FirstRunTimeline.tsx +4 -0
  5. package/src/app/input/AppInputProvider.tsx +19 -0
  6. package/src/app/input/appInputParser.ts +19 -4
  7. package/src/chat/ChatBottomPane.tsx +3 -1
  8. package/src/chat/ChatScreen.tsx +7 -1
  9. package/src/chat/ConversationStack.tsx +25 -19
  10. package/src/chat/MessageList.tsx +194 -53
  11. package/src/chat/chatSessionState.ts +1 -1
  12. package/src/chat/chatTurnOrchestrator.ts +59 -0
  13. package/src/chat/input/ChatInput.tsx +3 -0
  14. package/src/chat/input/textCursor.ts +13 -3
  15. package/src/chat/transcript/TranscriptView.tsx +7 -5
  16. package/src/chat/transcript/transcriptViewport.ts +88 -17
  17. package/src/chat/views/PermissionPrompt.tsx +26 -26
  18. package/src/chat/views/PermissionsView.tsx +18 -12
  19. package/src/chat/views/RewindView.tsx +3 -1
  20. package/src/cli/ResetConfirmView.tsx +24 -9
  21. package/src/identity/continuity/editor.ts +27 -2
  22. package/src/identity/continuity/envelope.ts +125 -0
  23. package/src/identity/continuity/publicSkills.ts +37 -1
  24. package/src/identity/continuity/skills/frontmatter.ts +183 -0
  25. package/src/identity/continuity/skills/loadSkills.ts +609 -0
  26. package/src/identity/continuity/skills/publicSkillsSync.ts +32 -0
  27. package/src/identity/continuity/skills/scaffold.ts +52 -0
  28. package/src/identity/continuity/skills/types.ts +30 -0
  29. package/src/identity/continuity/storage/defaults.ts +28 -47
  30. package/src/identity/continuity/storage/files.ts +1 -0
  31. package/src/identity/continuity/storage/paths.ts +1 -0
  32. package/src/identity/continuity/storage/scaffold.ts +25 -23
  33. package/src/identity/continuity/storage/status.ts +34 -5
  34. package/src/identity/continuity/storage/types.ts +3 -2
  35. package/src/identity/continuity/storage.ts +3 -0
  36. package/src/identity/hub/OperationalRoutes.tsx +105 -3
  37. package/src/identity/hub/Routes.tsx +5 -3
  38. package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +5 -51
  39. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +1 -1
  40. package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -0
  41. package/src/identity/hub/continuity/effects.ts +36 -5
  42. package/src/identity/hub/continuity/skills/DeleteSkillConfirmScreen.tsx +112 -0
  43. package/src/identity/hub/continuity/skills/DeleteSkillScreen.tsx +123 -0
  44. package/src/identity/hub/continuity/skills/NewSkillScreen.tsx +57 -0
  45. package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +52 -0
  46. package/src/identity/hub/continuity/skills/SkillVisibilityScreen.tsx +171 -0
  47. package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +213 -0
  48. package/src/identity/hub/continuity/snapshot.ts +3 -0
  49. package/src/identity/hub/continuity/state.ts +3 -2
  50. package/src/identity/hub/continuity/vault.ts +42 -10
  51. package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
  52. package/src/identity/hub/identityHubReducer.ts +21 -0
  53. package/src/identity/hub/profile/effects.ts +16 -3
  54. package/src/identity/hub/restore/RestoreFlow.tsx +43 -6
  55. package/src/identity/hub/restore/apply.ts +12 -1
  56. package/src/identity/hub/restore/recovery.ts +11 -1
  57. package/src/identity/hub/restore/resolve.ts +1 -1
  58. package/src/identity/hub/restore/useRestoreEffects.ts +4 -6
  59. package/src/identity/hub/shared/components/DetailsScreen.tsx +4 -1
  60. package/src/identity/hub/shared/components/IdentitySummary.tsx +97 -53
  61. package/src/identity/hub/shared/components/MenuScreen.tsx +18 -15
  62. package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +1 -1
  63. package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +8 -12
  64. package/src/identity/hub/shared/effects/sync.ts +16 -3
  65. package/src/identity/hub/shared/model/copy.ts +2 -4
  66. package/src/identity/hub/transfer/effects.ts +15 -2
  67. package/src/identity/hub/useIdentityHubContinuity.ts +145 -23
  68. package/src/identity/hub/useIdentityHubController.ts +5 -1
  69. package/src/identity/hub/useIdentityHubSideEffects.ts +2 -4
  70. package/src/mcp/manager.ts +1 -1
  71. package/src/models/ModelPicker.tsx +89 -84
  72. package/src/models/llamacpp.ts +160 -11
  73. package/src/models/llamacppPreflight.ts +1 -16
  74. package/src/models/modelPickerOptions.ts +43 -37
  75. package/src/providers/contracts.ts +1 -0
  76. package/src/providers/openai-chat.ts +50 -9
  77. package/src/providers/openai-responses.ts +19 -4
  78. package/src/runtime/toolExecution.ts +4 -3
  79. package/src/runtime/turn.ts +61 -30
  80. package/src/tools/changeDirectoryTool.ts +1 -1
  81. package/src/tools/contracts.ts +10 -0
  82. package/src/tools/deleteFileTool.ts +1 -1
  83. package/src/tools/editTool.ts +1 -1
  84. package/src/tools/listDirectoryTool.ts +1 -1
  85. package/src/tools/listSkillFilesTool.ts +77 -0
  86. package/src/tools/listSkillsTool.ts +68 -0
  87. package/src/tools/mcpResourceTools.ts +2 -2
  88. package/src/tools/privateContinuityReadTool.ts +1 -1
  89. package/src/tools/readSkillTool.ts +107 -0
  90. package/src/tools/readTool.ts +1 -1
  91. package/src/tools/registry.ts +6 -0
  92. package/src/tools/writeFileTool.ts +22 -2
  93. package/src/ui/Spinner.tsx +1 -1
  94. package/src/identity/continuity/localBackup.ts +0 -249
  95. package/src/identity/continuity/zipWriter.ts +0 -95
  96. package/src/identity/hub/continuity/index.ts +0 -7
  97. package/src/identity/hub/ens/index.ts +0 -11
  98. package/src/identity/hub/restore/index.ts +0 -22
@@ -0,0 +1,68 @@
1
+ import { z } from 'zod'
2
+ import {
3
+ continuityVaultRef,
4
+ } from '../identity/continuity/storage.js'
5
+ import { listSkillsTree } from '../identity/continuity/skills/loadSkills.js'
6
+ import type { Tool } from './contracts.js'
7
+
8
+ const schema = z.object({})
9
+
10
+ export const listSkillsTool: Tool<typeof schema> = {
11
+ name: 'list_private_skills',
12
+ kind: 'private-continuity-read',
13
+ readOnly: true,
14
+ description: [
15
+ 'List private skills available in the owner-authored skills tree for the active identity.',
16
+ 'Returns each skill folder name with its one-line description; bodies are loaded separately via read_private_skill.',
17
+ 'When a skill has supporting files beyond SKILL.md, the entry is annotated with (+N supporting files) so you know to call list_private_skill_files.',
18
+ 'Use this when the user mentions a skill not in the injected skill index, or to discover what skills exist.',
19
+ ].join(' '),
20
+ inputSchema: schema,
21
+ inputSchemaJson: {
22
+ type: 'object',
23
+ properties: {},
24
+ additionalProperties: false,
25
+ },
26
+ parse(input) {
27
+ return schema.parse(input ?? {})
28
+ },
29
+ async buildPermissionRequest(_input, context) {
30
+ const identity = context.config?.identity
31
+ if (!identity) throw new Error('No active identity; create or load an identity before listing private skills')
32
+ const ref = continuityVaultRef(identity)
33
+ return {
34
+ kind: 'private-skill-read',
35
+ path: ref.skillsDir,
36
+ relativePath: 'identity-vault/skills',
37
+ directoryPath: ref.skillsDir,
38
+ title: 'Allow private skills index read?',
39
+ subtitle: 'List skill names and descriptions from the private skills tree',
40
+ skillName: '*',
41
+ mode: 'list',
42
+ }
43
+ },
44
+ async execute(_input, context) {
45
+ const identity = context.config?.identity
46
+ if (!identity) {
47
+ return { ok: false, summary: 'no active identity', content: 'No active identity; cannot list private skills.' }
48
+ }
49
+ const { skills, supportingCounts } = await listSkillsTree(identity)
50
+ if (skills.length === 0) {
51
+ return { ok: true, summary: 'no private skills', content: 'The private skills tree is empty.' }
52
+ }
53
+ const lines = skills.map(entry => {
54
+ const display = entry.displayName ?? entry.name
55
+ const desc = entry.description ? ` — ${entry.description}` : ''
56
+ const when = entry.whenToUse ? ` (when: ${entry.whenToUse})` : ''
57
+ const vis = entry.visibility !== 'private' ? ` [visibility: ${entry.visibility}]` : ''
58
+ const supporting = supportingCounts[entry.name] ?? 0
59
+ const trailer = supporting > 0 ? ` (+${supporting} supporting file${supporting === 1 ? '' : 's'})` : ''
60
+ return `- ${display}${desc}${when}${vis}${trailer}`
61
+ })
62
+ return {
63
+ ok: true,
64
+ summary: `listed ${skills.length} private skill${skills.length === 1 ? '' : 's'}`,
65
+ content: lines.join('\n'),
66
+ }
67
+ },
68
+ }
@@ -30,7 +30,7 @@ export const listMcpResourcesTool: Tool<typeof ListMcpResourcesInput> = {
30
30
  const serverName = input.server ?? '*'
31
31
  return {
32
32
  kind: 'mcp',
33
- title: 'allow MCP resource listing?',
33
+ title: 'Allow MCP resource listing?',
34
34
  subtitle: input.server ? `list resources from ${input.server}` : 'list resources from all connected MCP servers',
35
35
  serverName,
36
36
  normalizedServerName: normalizeNameForMcp(serverName),
@@ -72,7 +72,7 @@ export const readMcpResourceTool: Tool<typeof ReadMcpResourceInput> = {
72
72
  async buildPermissionRequest(input) {
73
73
  return {
74
74
  kind: 'mcp',
75
- title: 'allow MCP resource read?',
75
+ title: 'Allow MCP resource read?',
76
76
  subtitle: `${input.server} / ${input.uri}`,
77
77
  serverName: input.server,
78
78
  normalizedServerName: normalizeNameForMcp(input.server),
@@ -43,7 +43,7 @@ export const privateContinuityReadTool: Tool<typeof schema> = {
43
43
  path: prepared.fullPath,
44
44
  relativePath: prepared.relativePath,
45
45
  directoryPath: prepared.directoryPath,
46
- title: 'allow private continuity read?',
46
+ title: 'Allow private continuity read?',
47
47
  subtitle: input.startLine || input.endLine
48
48
  ? `${prepared.fullPath} · lines ${input.startLine ?? 1}-${input.endLine ?? 'end'}`
49
49
  : prepared.fullPath,
@@ -0,0 +1,107 @@
1
+ import path from 'node:path'
2
+ import { z } from 'zod'
3
+ import {
4
+ continuityVaultRef,
5
+ } from '../identity/continuity/storage.js'
6
+ import {
7
+ readSkill,
8
+ readSkillFile,
9
+ } from '../identity/continuity/skills/loadSkills.js'
10
+ import type { Tool } from './contracts.js'
11
+
12
+ const schema = z.object({
13
+ name: z.string().min(1),
14
+ file: z.string().min(1).optional(),
15
+ })
16
+
17
+ export const readSkillTool: Tool<typeof schema> = {
18
+ name: 'read_private_skill',
19
+ kind: 'private-continuity-read',
20
+ readOnly: true,
21
+ description: [
22
+ 'Read a file from the owner-authored private skill folder for the active identity.',
23
+ 'Pass the skill folder name as `name` (e.g. "writing-obit"). Without `file`, returns the SKILL.md entry point.',
24
+ 'With `file`, returns a supporting file from the same folder (e.g. file: "references/api.md"). Supporting-file responses include an absolute filesystem `path=` attribute; run executable scripts via run_bash using that absolute path.',
25
+ 'List available supporting files with list_private_skill_files. Bodies are markdown and may include private instructions; treat them like SOUL.md guidance.',
26
+ ].join(' '),
27
+ inputSchema: schema,
28
+ inputSchemaJson: {
29
+ type: 'object',
30
+ properties: {
31
+ name: { type: 'string', description: 'Skill folder name from the private skills index, e.g. "writing-obit".' },
32
+ file: { type: 'string', description: 'Optional supporting file path inside the skill folder. Defaults to SKILL.md.' },
33
+ },
34
+ required: ['name'],
35
+ additionalProperties: false,
36
+ },
37
+ parse(input) {
38
+ return schema.parse(input)
39
+ },
40
+ async buildPermissionRequest(input, context) {
41
+ const identity = context.config?.identity
42
+ if (!identity) throw new Error('No active identity; create or load an identity before reading a private skill')
43
+ const ref = continuityVaultRef(identity)
44
+ const folder = input.name.replace(/^.*:/, '')
45
+ const file = input.file ?? 'SKILL.md'
46
+ const skillPath = path.join(ref.skillsDir, folder, file)
47
+ const display = input.file ? `${folder}/${input.file}` : `${folder}/SKILL.md`
48
+ return {
49
+ kind: 'private-skill-read',
50
+ path: skillPath,
51
+ relativePath: `identity-vault/skills/${display}`,
52
+ directoryPath: path.dirname(skillPath),
53
+ title: 'Allow private skill read?',
54
+ subtitle: `Load ${display}`,
55
+ skillName: input.name,
56
+ mode: 'read',
57
+ }
58
+ },
59
+ async execute(input, context) {
60
+ const identity = context.config?.identity
61
+ if (!identity) {
62
+ return { ok: false, summary: 'no active identity', content: 'No active identity; cannot read private skill.' }
63
+ }
64
+ try {
65
+ const folder = input.name.replace(/^.*:/, '')
66
+ if (input.file && input.file !== 'SKILL.md') {
67
+ const result = await readSkillFile(identity, folder, input.file)
68
+ return {
69
+ ok: true,
70
+ summary: `read ${result.relativePath}`,
71
+ content: [
72
+ `<private_skill_file name="${folder}" file="${input.file}" path="${result.absolutePath}">`,
73
+ result.content,
74
+ '</private_skill_file>',
75
+ ].join('\n'),
76
+ }
77
+ }
78
+ const skill = await readSkill(identity, folder)
79
+ const meta: string[] = []
80
+ meta.push(`name: ${skill.displayName ?? skill.name}`)
81
+ if (skill.whenToUse) meta.push(`when_to_use: ${skill.whenToUse}`)
82
+ if (skill.argumentHint) meta.push(`argument_hint: ${skill.argumentHint}`)
83
+ if (skill.version) meta.push(`version: ${skill.version}`)
84
+ if (skill.tags && skill.tags.length > 0) meta.push(`tags: ${skill.tags.join(', ')}`)
85
+ meta.push(`visibility: ${skill.visibility}`)
86
+ const content = [
87
+ `<private_skill name="${skill.name}" visibility="${skill.visibility}">`,
88
+ ...meta,
89
+ '',
90
+ skill.body,
91
+ '</private_skill>',
92
+ ].join('\n')
93
+ return {
94
+ ok: true,
95
+ summary: `read private skill ${skill.name}`,
96
+ content,
97
+ }
98
+ } catch (err: unknown) {
99
+ const message = (err as Error).message
100
+ return {
101
+ ok: false,
102
+ summary: 'read failed',
103
+ content: message,
104
+ }
105
+ }
106
+ },
107
+ }
@@ -37,7 +37,7 @@ export const readTool: Tool<typeof schema> = {
37
37
  path: fullPath,
38
38
  relativePath,
39
39
  directoryPath: path.dirname(fullPath),
40
- title: 'allow file read?',
40
+ title: 'Allow file read?',
41
41
  subtitle: input.startLine || input.endLine
42
42
  ? `${fullPath} · lines ${input.startLine ?? 1}-${input.endLine ?? 'end'}`
43
43
  : fullPath,
@@ -8,8 +8,11 @@ import { changeDirectoryTool } from './changeDirectoryTool.js'
8
8
  import { deleteFileTool } from './deleteFileTool.js'
9
9
  import { editTool } from './editTool.js'
10
10
  import { listDirectoryTool } from './listDirectoryTool.js'
11
+ import { listSkillsTool } from './listSkillsTool.js'
12
+ import { listSkillFilesTool } from './listSkillFilesTool.js'
11
13
  import { privateContinuityEditTool } from './privateContinuityEditTool.js'
12
14
  import { privateContinuityReadTool } from './privateContinuityReadTool.js'
15
+ import { readSkillTool } from './readSkillTool.js'
13
16
  import { readTool } from './readTool.js'
14
17
  import { listMcpResourcesTool, readMcpResourceTool } from './mcpResourceTools.js'
15
18
  import { writeFileTool } from './writeFileTool.js'
@@ -19,6 +22,9 @@ export const BUILTIN_TOOLS: Tool[] = [
19
22
  listDirectoryTool,
20
23
  readTool,
21
24
  privateContinuityReadTool,
25
+ listSkillsTool,
26
+ listSkillFilesTool,
27
+ readSkillTool,
22
28
  listMcpResourcesTool,
23
29
  readMcpResourceTool,
24
30
  writeFileTool,
@@ -28,7 +28,7 @@ export const writeFileTool: Tool<typeof schema> = {
28
28
  required: ['path', 'content'],
29
29
  },
30
30
  parse(input) {
31
- return schema.parse(input)
31
+ return schema.parse(normalizeWriteFileInput(input))
32
32
  },
33
33
  async buildPermissionRequest(input, context) {
34
34
  const prepared = await prepareWrite(input, context)
@@ -37,7 +37,7 @@ export const writeFileTool: Tool<typeof schema> = {
37
37
  path: prepared.fullPath,
38
38
  relativePath: prepared.relativePath,
39
39
  directoryPath: path.dirname(prepared.fullPath),
40
- title: prepared.existedBefore ? 'allow file rewrite?' : 'allow file creation?',
40
+ title: prepared.existedBefore ? 'Allow file rewrite?' : 'Allow file creation?',
41
41
  subtitle: prepared.fullPath,
42
42
  before: previewText(prepared.before),
43
43
  after: previewText(input.content),
@@ -76,6 +76,26 @@ export const writeFileTool: Tool<typeof schema> = {
76
76
  },
77
77
  }
78
78
 
79
+ function normalizeWriteFileInput(input: unknown): unknown {
80
+ let value: unknown = input
81
+ if (typeof value === 'string') {
82
+ const trimmed = value.trim()
83
+ if (trimmed.startsWith('{')) {
84
+ try {
85
+ value = JSON.parse(trimmed)
86
+ } catch {
87
+ return input
88
+ }
89
+ }
90
+ }
91
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value
92
+ const normalized: Record<string, unknown> = { ...(value as Record<string, unknown>) }
93
+ if (typeof normalized.path === 'string' && normalized.path.trim() === '') {
94
+ normalized.path = undefined
95
+ }
96
+ return normalized
97
+ }
98
+
79
99
  async function prepareWrite(
80
100
  input: z.infer<typeof schema>,
81
101
  context: { workspaceRoot: string; config?: EthagentConfig },
@@ -243,7 +243,7 @@ export const Spinner: React.FC<SpinnerProps> = ({
243
243
  useEffect(() => {
244
244
  if (!active) return
245
245
  internalStartedAtRef.current = startedAt ?? Date.now()
246
- }, [active, startedAt, label, verb])
246
+ }, [active, startedAt])
247
247
 
248
248
  useEffect(() => {
249
249
  if (!active) return
@@ -1,249 +0,0 @@
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
- }
@@ -1,95 +0,0 @@
1
- export type ZipEntry = {
2
- name: string
3
- data: Buffer
4
- }
5
-
6
- const crcTable = (() => {
7
- const table = new Uint32Array(256)
8
- for (let i = 0; i < 256; i++) {
9
- let c = i
10
- for (let k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)
11
- table[i] = c
12
- }
13
- return table
14
- })()
15
-
16
- export function crc32(buf: Buffer): number {
17
- let c = 0xFFFFFFFF
18
- for (let i = 0; i < buf.length; i++) {
19
- const byte = buf[i] as number
20
- c = (crcTable[(c ^ byte) & 0xff] as number) ^ (c >>> 8)
21
- }
22
- return (c ^ 0xFFFFFFFF) >>> 0
23
- }
24
-
25
- export function buildZip(entries: ZipEntry[], modifiedAt: Date = new Date()): Buffer {
26
- const dosTime = toDosTime(modifiedAt)
27
- const dosDate = toDosDate(modifiedAt)
28
- const localParts: Buffer[] = []
29
- const centralParts: Buffer[] = []
30
- let offset = 0
31
- for (const entry of entries) {
32
- const nameBuf = Buffer.from(entry.name, 'utf8')
33
- const crc = crc32(entry.data)
34
- const size = entry.data.length
35
-
36
- const localHeader = Buffer.alloc(30)
37
- localHeader.writeUInt32LE(0x04034b50, 0)
38
- localHeader.writeUInt16LE(20, 4)
39
- localHeader.writeUInt16LE(0x0800, 6)
40
- localHeader.writeUInt16LE(0, 8)
41
- localHeader.writeUInt16LE(dosTime, 10)
42
- localHeader.writeUInt16LE(dosDate, 12)
43
- localHeader.writeUInt32LE(crc, 14)
44
- localHeader.writeUInt32LE(size, 18)
45
- localHeader.writeUInt32LE(size, 22)
46
- localHeader.writeUInt16LE(nameBuf.length, 26)
47
- localHeader.writeUInt16LE(0, 28)
48
- localParts.push(localHeader, nameBuf, entry.data)
49
-
50
- const centralHeader = Buffer.alloc(46)
51
- centralHeader.writeUInt32LE(0x02014b50, 0)
52
- centralHeader.writeUInt16LE(20, 4)
53
- centralHeader.writeUInt16LE(20, 6)
54
- centralHeader.writeUInt16LE(0x0800, 8)
55
- centralHeader.writeUInt16LE(0, 10)
56
- centralHeader.writeUInt16LE(dosTime, 12)
57
- centralHeader.writeUInt16LE(dosDate, 14)
58
- centralHeader.writeUInt32LE(crc, 16)
59
- centralHeader.writeUInt32LE(size, 20)
60
- centralHeader.writeUInt32LE(size, 24)
61
- centralHeader.writeUInt16LE(nameBuf.length, 28)
62
- centralHeader.writeUInt16LE(0, 30)
63
- centralHeader.writeUInt16LE(0, 32)
64
- centralHeader.writeUInt16LE(0, 34)
65
- centralHeader.writeUInt16LE(0, 36)
66
- centralHeader.writeUInt32LE(0, 38)
67
- centralHeader.writeUInt32LE(offset, 42)
68
- centralParts.push(centralHeader, nameBuf)
69
-
70
- offset += localHeader.length + nameBuf.length + entry.data.length
71
- }
72
-
73
- const centralBuf = Buffer.concat(centralParts)
74
- const localBuf = Buffer.concat(localParts)
75
- const eocd = Buffer.alloc(22)
76
- eocd.writeUInt32LE(0x06054b50, 0)
77
- eocd.writeUInt16LE(0, 4)
78
- eocd.writeUInt16LE(0, 6)
79
- eocd.writeUInt16LE(entries.length, 8)
80
- eocd.writeUInt16LE(entries.length, 10)
81
- eocd.writeUInt32LE(centralBuf.length, 12)
82
- eocd.writeUInt32LE(localBuf.length, 16)
83
- eocd.writeUInt16LE(0, 20)
84
-
85
- return Buffer.concat([localBuf, centralBuf, eocd])
86
- }
87
-
88
- function toDosTime(date: Date): number {
89
- return ((date.getHours() & 0x1f) << 11) | ((date.getMinutes() & 0x3f) << 5) | ((date.getSeconds() / 2) & 0x1f)
90
- }
91
-
92
- function toDosDate(date: Date): number {
93
- const year = Math.max(1980, date.getFullYear()) - 1980
94
- return ((year & 0x7f) << 9) | (((date.getMonth() + 1) & 0x0f) << 5) | (date.getDate() & 0x1f)
95
- }
@@ -1,7 +0,0 @@
1
- export {
2
- rebackupCompletionMessage,
3
- runRebackupPreflight,
4
- runRebackupSigning,
5
- runRebackupSigningInSession,
6
- runRebackupStorageSubmit,
7
- } from './effects.js'
@@ -1,11 +0,0 @@
1
- export {
2
- runEnsLinkFlow,
3
- runEnsUnlinkFlow,
4
- runEnsUpdateFlow,
5
- } from './effects.js'
6
- export {
7
- ensRecordWritesForUpdate,
8
- runEnsSetupRecordsTransaction,
9
- runEnsSetupRegistryTransaction,
10
- runUpdateEnsRecords,
11
- } from './transactions.js'
@@ -1,22 +0,0 @@
1
- export {
2
- runRestoreAuthorize,
3
- } from './apply.js'
4
- export {
5
- restoreSignatureRequestForStep,
6
- runRestoreConnectWallet,
7
- } from './auth.js'
8
- export {
9
- canRestoreCandidate,
10
- restoreTokenSelectionStep,
11
- runRestoreDiscover,
12
- } from './discover.js'
13
- export {
14
- runRestoreFetch,
15
- } from './fetch.js'
16
- export {
17
- runRecoveryRefetch,
18
- } from './recovery.js'
19
- export {
20
- resolveAgentEnsToCandidate,
21
- resolveAgentTokenIdToCandidate,
22
- } from './resolve.js'