ethagent 2.4.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/package.json +2 -1
- package/src/app/FirstRun.tsx +155 -15
- package/src/app/FirstRunTimeline.tsx +4 -0
- package/src/app/input/AppInputProvider.tsx +19 -0
- package/src/app/input/appInputParser.ts +19 -4
- package/src/chat/ChatBottomPane.tsx +3 -1
- package/src/chat/ChatScreen.tsx +7 -1
- package/src/chat/ConversationStack.tsx +25 -19
- package/src/chat/MessageList.tsx +194 -53
- package/src/chat/chatSessionState.ts +1 -1
- package/src/chat/chatTurnOrchestrator.ts +59 -0
- package/src/chat/input/ChatInput.tsx +3 -0
- package/src/chat/input/textCursor.ts +13 -3
- package/src/chat/transcript/TranscriptView.tsx +7 -5
- package/src/chat/transcript/transcriptViewport.ts +88 -17
- package/src/chat/views/PermissionPrompt.tsx +26 -26
- package/src/chat/views/PermissionsView.tsx +18 -12
- package/src/chat/views/RewindView.tsx +3 -1
- package/src/cli/ResetConfirmView.tsx +24 -9
- package/src/identity/continuity/editor.ts +27 -2
- package/src/identity/continuity/envelope.ts +134 -9
- package/src/identity/continuity/publicSkills.ts +54 -1
- package/src/identity/continuity/skills/frontmatter.ts +183 -0
- package/src/identity/continuity/skills/loadSkills.ts +609 -0
- package/src/identity/continuity/skills/publicSkillsSync.ts +32 -0
- package/src/identity/continuity/skills/scaffold.ts +52 -0
- package/src/identity/continuity/skills/types.ts +30 -0
- package/src/identity/continuity/storage/defaults.ts +28 -47
- package/src/identity/continuity/storage/files.ts +1 -0
- package/src/identity/continuity/storage/paths.ts +1 -0
- package/src/identity/continuity/storage/scaffold.ts +25 -23
- package/src/identity/continuity/storage/status.ts +34 -5
- package/src/identity/continuity/storage/types.ts +3 -2
- package/src/identity/continuity/storage.ts +3 -0
- package/src/identity/hub/OperationalRoutes.tsx +79 -5
- package/src/identity/hub/Routes.tsx +5 -3
- package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +7 -73
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +6 -6
- package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -2
- package/src/identity/hub/continuity/effects.ts +36 -5
- package/src/identity/hub/continuity/skills/DeleteSkillConfirmScreen.tsx +112 -0
- package/src/identity/hub/continuity/skills/NewSkillScreen.tsx +57 -0
- package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +52 -0
- package/src/identity/hub/continuity/skills/SkillActionsScreen.tsx +151 -0
- package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +181 -0
- package/src/identity/hub/continuity/snapshot.ts +3 -0
- package/src/identity/hub/continuity/state.ts +9 -8
- package/src/identity/hub/continuity/vault.ts +42 -10
- package/src/identity/hub/create/CreateFlow.tsx +1 -1
- package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
- package/src/identity/hub/custody/routes.tsx +1 -1
- package/src/identity/hub/ens/EnsEditAdvancedScreens.tsx +0 -1
- package/src/identity/hub/ens/EnsEditMaintenanceScreens.tsx +0 -1
- package/src/identity/hub/identityHubReducer.ts +15 -0
- package/src/identity/hub/profile/EditProfileFlow.tsx +5 -5
- package/src/identity/hub/profile/effects.ts +16 -3
- package/src/identity/hub/restore/RestoreFlow.tsx +43 -6
- package/src/identity/hub/restore/apply.ts +12 -1
- package/src/identity/hub/restore/recovery.ts +14 -4
- package/src/identity/hub/restore/resolve.ts +1 -1
- package/src/identity/hub/restore/useRestoreEffects.ts +4 -6
- package/src/identity/hub/shared/components/DetailsScreen.tsx +4 -1
- package/src/identity/hub/shared/components/IdentitySummary.tsx +118 -54
- package/src/identity/hub/shared/components/MenuScreen.tsx +21 -18
- package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +4 -4
- package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +8 -12
- package/src/identity/hub/shared/effects/sync.ts +16 -3
- package/src/identity/hub/shared/model/copy.ts +2 -4
- package/src/identity/hub/transfer/effects.ts +15 -2
- package/src/identity/hub/useIdentityHubContinuity.ts +145 -23
- package/src/identity/hub/useIdentityHubController.ts +5 -1
- package/src/identity/hub/useIdentityHubSideEffects.ts +2 -4
- package/src/identity/wallet/page/copy.ts +43 -43
- package/src/mcp/manager.ts +1 -1
- package/src/models/ModelPicker.tsx +89 -84
- package/src/models/llamacpp.ts +160 -11
- package/src/models/llamacppPreflight.ts +1 -16
- package/src/models/modelPickerOptions.ts +45 -37
- package/src/providers/contracts.ts +1 -0
- package/src/providers/openai-chat.ts +50 -9
- package/src/providers/openai-responses.ts +19 -4
- package/src/runtime/toolExecution.ts +4 -3
- package/src/runtime/turn.ts +61 -30
- package/src/tools/changeDirectoryTool.ts +1 -1
- package/src/tools/contracts.ts +10 -0
- package/src/tools/deleteFileTool.ts +1 -1
- package/src/tools/editTool.ts +1 -1
- package/src/tools/listDirectoryTool.ts +1 -1
- package/src/tools/listSkillFilesTool.ts +77 -0
- package/src/tools/listSkillsTool.ts +68 -0
- package/src/tools/mcpResourceTools.ts +2 -2
- package/src/tools/privateContinuityReadTool.ts +1 -1
- package/src/tools/readSkillTool.ts +107 -0
- package/src/tools/readTool.ts +1 -1
- package/src/tools/registry.ts +6 -0
- package/src/tools/writeFileTool.ts +22 -2
- package/src/ui/Spinner.tsx +1 -1
- package/src/identity/continuity/localBackup.ts +0 -249
- package/src/identity/continuity/zipWriter.ts +0 -95
- package/src/identity/hub/continuity/index.ts +0 -7
- package/src/identity/hub/ens/index.ts +0 -11
- 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: '
|
|
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: '
|
|
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: '
|
|
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
|
+
}
|
package/src/tools/readTool.ts
CHANGED
|
@@ -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: '
|
|
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,
|
package/src/tools/registry.ts
CHANGED
|
@@ -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 ? '
|
|
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 },
|
package/src/ui/Spinner.tsx
CHANGED
|
@@ -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
|
|
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,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'
|