@worldzb/agent-sync 1.4.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 (46) hide show
  1. package/README.md +176 -0
  2. package/dist/agents/abstract-file-adapter.js +53 -0
  3. package/dist/agents/claude-adapter.js +154 -0
  4. package/dist/agents/codex-adapter.js +139 -0
  5. package/dist/agents/jsonl.js +26 -0
  6. package/dist/agents/opencode-adapter.js +193 -0
  7. package/dist/agents/process-runner.js +33 -0
  8. package/dist/agents/registry.js +23 -0
  9. package/dist/agents/types.js +1 -0
  10. package/dist/claude/settings.js +52 -0
  11. package/dist/cli/create-program.js +32 -0
  12. package/dist/commands/add.js +70 -0
  13. package/dist/commands/agents.js +16 -0
  14. package/dist/commands/context.js +1 -0
  15. package/dist/commands/current.js +32 -0
  16. package/dist/commands/delete.js +47 -0
  17. package/dist/commands/history.js +155 -0
  18. package/dist/commands/integrations.js +77 -0
  19. package/dist/commands/list.js +31 -0
  20. package/dist/commands/migrate.js +42 -0
  21. package/dist/commands/sessions.js +52 -0
  22. package/dist/commands/switch.js +93 -0
  23. package/dist/config/config-data.js +85 -0
  24. package/dist/config/config-repository.js +29 -0
  25. package/dist/config/paths.js +16 -0
  26. package/dist/config/types.js +1 -0
  27. package/dist/history/session-loader-worker.js +23 -0
  28. package/dist/history/session-scope.js +10 -0
  29. package/dist/history/session-service.js +29 -0
  30. package/dist/history/ui/formatters.js +37 -0
  31. package/dist/history/ui/history-app.js +262 -0
  32. package/dist/history/ui/key-hints.js +6 -0
  33. package/dist/history/ui/navigation.js +11 -0
  34. package/dist/history/ui/session-details.js +8 -0
  35. package/dist/history/ui/session-list.js +16 -0
  36. package/dist/history/ui/theme.js +14 -0
  37. package/dist/history/ui/viewport.js +13 -0
  38. package/dist/index.js +27 -0
  39. package/dist/migration/migration-service.js +55 -0
  40. package/dist/migration/prompt-renderer.js +27 -0
  41. package/dist/migration/transcript-normalizer.js +53 -0
  42. package/dist/sessions/managed-session-repository.js +56 -0
  43. package/dist/sessions/session-launcher.js +63 -0
  44. package/dist/sessions/session-monitor.js +16 -0
  45. package/dist/ui/output.js +13 -0
  46. package/package.json +52 -0
package/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # ZMAI
2
+
3
+ `zmai` 是一个本机多 Agent 历史会话工作台,同时提供 Claude API 配置管理能力。
4
+
5
+ 支持发现和管理本机的 **Claude Code**、**Codex**、**OpenCode**:浏览历史会话、按项目和 Agent 分类、续接会话、跨 Agent 导入历史、迁移附件,以及查看 plugins、Skills、MCP。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ npm install -g @worldzb/zmai
11
+ ```
12
+
13
+ 安装后使用 `zmai` 命令。
14
+
15
+ > 需要 Node.js 18 或更高版本。
16
+
17
+ ## 核心功能
18
+
19
+ | 功能 | 说明 |
20
+ | --- | --- |
21
+ | Agent 发现 | 扫描 Claude Code、Codex、OpenCode 的安装状态、版本与本地数据目录 |
22
+ | 历史会话 | 统一读取三个 Agent 的本地会话,按时间排序、分类和分页浏览 |
23
+ | 项目筛选 | 区分当前项目会话与全部会话 |
24
+ | Agent 筛选 | 按全部、Claude、Codex、OpenCode 分类显示 |
25
+ | 会话续接 | 用原 Agent 继续历史会话 |
26
+ | 跨 Agent 迁移 | 将历史转换为可移植上下文,并在目标 Agent 中创建新会话 |
27
+ | 附件迁移 | 复制可读取的图片/文件,生成带校验值的迁移清单 |
28
+ | 集成管理 | 查看 plugins、Skills、MCP,并调用 Agent 原生命令安装或移除 |
29
+ | API 配置 | 管理 Claude API Key、Base URL、默认配置和临时环境变量 |
30
+
31
+ ## 扫描 Agent
32
+
33
+ ```bash
34
+ zmai agents
35
+ ```
36
+
37
+ 输出每个 Agent 的:
38
+
39
+ - 是否已安装及可执行命令路径
40
+ - 版本号
41
+ - 本地历史目录
42
+ - 支持的会话、集成与配置能力
43
+
44
+ ## 历史会话
45
+
46
+ ```bash
47
+ zmai history
48
+ ```
49
+
50
+ 交互界面由 React + Ink 渲染。初次加载会动态显示 Claude Code、Codex、OpenCode 的扫描进度。
51
+
52
+ ### 范围与分类
53
+
54
+ - `Tab`、左方向键:当前项目 / 全部会话之间切换
55
+ - 右方向键:切换至全部会话
56
+ - `1`:全部 Agent
57
+ - `2`:Claude Code 会话
58
+ - `3`:Codex 会话
59
+ - `4`:OpenCode 会话
60
+
61
+ ### 会话操作
62
+
63
+ - 上下方向键:选择会话;在列表首尾可自动跨页
64
+ - `Ctrl+U` 或 `-`:上一页
65
+ - `Ctrl+D` 或 `=`:下一页
66
+ - `Enter`:打开会话操作
67
+ - `r`:重新扫描本地历史
68
+ - `q`:退出
69
+
70
+ 非交互环境可使用文本输出:
71
+
72
+ ```bash
73
+ zmai history --plain
74
+ zmai history --plain --agent claude --page-size 30
75
+ ```
76
+
77
+ ## 续接与跨 Agent 迁移
78
+
79
+ 在历史会话中按 `Enter` 后:
80
+
81
+ - **选择原 Agent**:在当前终端直接续接原生会话。
82
+ - **选择其他 Agent**:创建迁移目录,转换旧会话内容,然后在当前终端启动目标 Agent。
83
+
84
+ 迁移时会:
85
+
86
+ 1. 将用户、助手与工具消息转换为可移植 Markdown 上下文;
87
+ 2. 复制可读取且不超过 20MB 的图片和文件;
88
+ 3. 为附件生成 SHA-256 校验值;
89
+ 4. 生成 `conversation.md` 和 `manifest.json`;
90
+ 5. 将完整历史复制到剪贴板,并将上下文交给目标 Agent 新会话。
91
+
92
+ 迁移文件保存于:
93
+
94
+ ```text
95
+ ~/.zmai/migrations/<migration-id>/
96
+ ```
97
+
98
+ 来源会话不会被修改或删除。
99
+
100
+ 也可通过命令迁移:
101
+
102
+ ```bash
103
+ zmai migrate codex:<session-id> --to claude
104
+ ```
105
+
106
+ ## Plugins、Skills 与 MCP
107
+
108
+ ```bash
109
+ # 查看全部 Agent 的资源
110
+ zmai integrations
111
+
112
+ # 仅查看某个 Agent,并包含项目级资源
113
+ zmai integrations --agent claude --project /path/to/project
114
+
115
+ # 安装插件
116
+ zmai integrations --agent claude --install-plugin plugin@marketplace
117
+
118
+ # 移除资源
119
+ zmai integrations --remove claude:plugin:plugin-name
120
+ ```
121
+
122
+ 安装或移除前会要求明确确认。插件和 MCP 优先使用对应 Agent 的原生命令;不会自动执行未知远程脚本。
123
+
124
+ ## Claude API 配置
125
+
126
+ ```bash
127
+ # 交互式添加配置
128
+ zmai add -i
129
+
130
+ # 命令行添加配置
131
+ zmai add -n "官方 API" -k "sk-ant-api03-xxx" -u "https://api.anthropic.com"
132
+
133
+ # 查看配置
134
+ zmai list
135
+
136
+ # 当前终端临时使用
137
+ eval $(zmai switch -n "官方 API" --temp --eval)
138
+
139
+ # 设为 Claude Code 默认配置
140
+ zmai switch -n "官方 API"
141
+
142
+ # 查看当前 Claude 配置
143
+ zmai current
144
+ ```
145
+
146
+ 默认配置会更新:
147
+
148
+ ```text
149
+ ~/.claude/settings.json
150
+ ~/.claude-switch-config/.claude-env
151
+ ```
152
+
153
+ ## 数据目录
154
+
155
+ | 数据 | 路径 |
156
+ | --- | --- |
157
+ | Claude API 配置 | `~/.claude-switch-config/claude-configs.json` |
158
+ | 临时环境变量 | `~/.claude-switch-config/.claude-env` |
159
+ | Claude Code 设置 | `~/.claude/settings.json` |
160
+ | 会话迁移上下文与附件 | `~/.zmai/migrations/` |
161
+ | 旧版托管会话记录 | `~/.zmai/sessions.json` |
162
+
163
+ ## 开发
164
+
165
+ ```bash
166
+ npm install
167
+ npm run check
168
+ npm test
169
+ npm run build
170
+ ```
171
+
172
+ 源码在 `src/`,构建产物在 `dist/`。
173
+
174
+ ## 许可证
175
+
176
+ MIT
@@ -0,0 +1,53 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { findExecutable, getVersion, runCommand } from './process-runner.js';
4
+ export class AbstractFileAdapter {
5
+ discover() {
6
+ const executable = findExecutable(this.command);
7
+ return {
8
+ id: this.id,
9
+ name: this.name,
10
+ installed: executable !== undefined,
11
+ ...(executable ? { executable, version: getVersion(executable) } : {}),
12
+ historyRoot: this.historyRoot,
13
+ capabilities: ['历史记录', '续接会话', '删除会话', '插件', 'Skills', 'MCP'],
14
+ };
15
+ }
16
+ listIntegrations(project) {
17
+ const roots = this.integrationRoots(project);
18
+ return roots.flatMap(({ directory, scope }) => scanIntegrationRoot(this.id, directory, scope));
19
+ }
20
+ execute(args, cwd) {
21
+ const executable = findExecutable(this.command);
22
+ if (!executable)
23
+ throw new Error(`${this.name} 未安装或不在 PATH 中。`);
24
+ return runCommand(executable, args, cwd);
25
+ }
26
+ removeLocalPath(location) {
27
+ if (!fs.existsSync(location))
28
+ throw new Error(`资源不存在:${location}`);
29
+ fs.rmSync(location, { recursive: true, force: false });
30
+ }
31
+ }
32
+ function scanIntegrationRoot(agent, directory, scope) {
33
+ if (!fs.existsSync(directory))
34
+ return [];
35
+ const candidates = [
36
+ { name: 'skills', kind: 'skill' },
37
+ ];
38
+ return candidates.flatMap(({ name, kind }) => {
39
+ const root = path.join(directory, name);
40
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory())
41
+ return [];
42
+ return fs.readdirSync(root, { withFileTypes: true })
43
+ .filter((entry) => !entry.name.startsWith('.') && (entry.isDirectory() || entry.isFile()))
44
+ .map((entry) => ({
45
+ agent,
46
+ kind,
47
+ name: entry.name,
48
+ scope,
49
+ location: path.join(root, entry.name),
50
+ removable: true,
51
+ }));
52
+ });
53
+ }
@@ -0,0 +1,154 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { AbstractFileAdapter } from './abstract-file-adapter.js';
4
+ import { asString, isRecord, readJsonLines } from './jsonl.js';
5
+ import { normalizeMessage } from '../migration/transcript-normalizer.js';
6
+ export class ClaudeAdapter extends AbstractFileAdapter {
7
+ homeDirectory;
8
+ id = 'claude';
9
+ name = 'Claude Code';
10
+ command = 'claude';
11
+ historyRoot;
12
+ constructor(homeDirectory) {
13
+ super();
14
+ this.homeDirectory = homeDirectory;
15
+ this.historyRoot = path.join(homeDirectory, '.claude', 'projects');
16
+ }
17
+ listSessions() {
18
+ return findFiles(this.historyRoot, (file) => file.endsWith('.jsonl')).flatMap((sourcePath) => {
19
+ const rows = readJsonLines(sourcePath).filter(isRecord);
20
+ const first = rows[0];
21
+ if (!first)
22
+ return [];
23
+ const id = asString(first.sessionId) || path.basename(sourcePath, '.jsonl');
24
+ const firstMessage = rows.map(extractMessage).find((message) => message?.role === 'user');
25
+ const cwd = rows.map((row) => asString(row.cwd)).find((value) => Boolean(value));
26
+ const updatedAt = asString(rows.at(-1)?.timestamp) || fs.statSync(sourcePath).mtime.toISOString();
27
+ return [{
28
+ agent: this.id,
29
+ id,
30
+ title: sanitizeTitle(firstText(firstMessage)) || '未命名 Claude 会话',
31
+ cwd: cwd || decodeProjectDirectory(path.dirname(sourcePath)),
32
+ updatedAt,
33
+ sourcePath,
34
+ }];
35
+ });
36
+ }
37
+ readTranscript(session) {
38
+ const messages = readJsonLines(session.sourcePath).flatMap((row) => {
39
+ const message = extractMessage(row);
40
+ return message ? [message] : [];
41
+ });
42
+ return { source: session, messages, warnings: [] };
43
+ }
44
+ createResumeLaunch(session) {
45
+ return { command: [this.command, '--resume', session.id], cwd: session.cwd || process.cwd() };
46
+ }
47
+ createNewLaunch(input) {
48
+ const assetArgument = input.assetDirectory ? ['--add-dir', input.assetDirectory] : [];
49
+ const promptArgument = input.prompt ? [input.prompt] : [];
50
+ return { command: [this.command, ...assetArgument, ...promptArgument], cwd: input.cwd || process.cwd() };
51
+ }
52
+ deleteSession(session) {
53
+ this.removeLocalPath(session.sourcePath);
54
+ }
55
+ integrationRoots(project) {
56
+ return [
57
+ { directory: path.join(this.homeDirectory, '.claude'), scope: 'user' },
58
+ ...(project ? [{ directory: project, scope: 'project' }] : []),
59
+ ];
60
+ }
61
+ listIntegrations(project) {
62
+ return [...super.listIntegrations(project), ...this.listInstalledPlugins(), ...this.listMcp(project)];
63
+ }
64
+ installPlugin(plugin) {
65
+ this.execute(['plugin', 'install', plugin]);
66
+ }
67
+ removeIntegration(item) {
68
+ if (item.kind === 'plugin') {
69
+ this.execute(['plugin', 'uninstall', item.name]);
70
+ return;
71
+ }
72
+ if (item.kind === 'mcp') {
73
+ this.execute(['mcp', 'remove', item.name]);
74
+ return;
75
+ }
76
+ this.removeLocalPath(item.location);
77
+ }
78
+ listInstalledPlugins() {
79
+ const file = path.join(this.homeDirectory, '.claude', 'plugins', 'installed_plugins.json');
80
+ if (!fs.existsSync(file))
81
+ return [];
82
+ try {
83
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
84
+ if (!isRecord(parsed) || !isRecord(parsed.plugins))
85
+ return [];
86
+ return Object.keys(parsed.plugins).map((name) => ({
87
+ agent: this.id,
88
+ kind: 'plugin',
89
+ name,
90
+ scope: 'user',
91
+ location: file,
92
+ removable: true,
93
+ }));
94
+ }
95
+ catch {
96
+ return [];
97
+ }
98
+ }
99
+ listMcp(project) {
100
+ const configFiles = [
101
+ path.join(this.homeDirectory, '.claude.json'),
102
+ ...(project ? [path.join(project, '.mcp.json')] : []),
103
+ ];
104
+ return configFiles.flatMap((file) => {
105
+ if (!fs.existsSync(file))
106
+ return [];
107
+ try {
108
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
109
+ if (!isRecord(parsed) || !isRecord(parsed.mcpServers))
110
+ return [];
111
+ const scope = file.endsWith('.mcp.json') ? 'project' : 'user';
112
+ return Object.keys(parsed.mcpServers).map((name) => ({
113
+ agent: this.id,
114
+ kind: 'mcp',
115
+ name,
116
+ scope,
117
+ location: file,
118
+ removable: true,
119
+ }));
120
+ }
121
+ catch {
122
+ return [];
123
+ }
124
+ });
125
+ }
126
+ }
127
+ function extractMessage(row) {
128
+ if (!isRecord(row))
129
+ return undefined;
130
+ return normalizeMessage(row.message);
131
+ }
132
+ function firstText(message) {
133
+ const text = message?.content.find((content) => content.type === 'text');
134
+ return text?.type === 'text' ? text.text.replaceAll(/\s+/g, ' ').slice(0, 100) : undefined;
135
+ }
136
+ function sanitizeTitle(value) {
137
+ if (!value || /<(local-command-caveat|system-reminder|command-message)>|^\s*[\[{]/i.test(value)) {
138
+ return undefined;
139
+ }
140
+ return value;
141
+ }
142
+ function decodeProjectDirectory(directory) {
143
+ return path.basename(directory).replaceAll('-', path.sep).replace(/^\/+/, path.sep);
144
+ }
145
+ function findFiles(directory, match) {
146
+ if (!fs.existsSync(directory))
147
+ return [];
148
+ return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
149
+ const entryPath = path.join(directory, entry.name);
150
+ if (entry.isDirectory())
151
+ return findFiles(entryPath, match);
152
+ return entry.isFile() && match(entryPath) ? [entryPath] : [];
153
+ });
154
+ }
@@ -0,0 +1,139 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { AbstractFileAdapter } from './abstract-file-adapter.js';
4
+ import { asString, isRecord, readJsonLines } from './jsonl.js';
5
+ import { normalizeMessage } from '../migration/transcript-normalizer.js';
6
+ export class CodexAdapter extends AbstractFileAdapter {
7
+ homeDirectory;
8
+ id = 'codex';
9
+ name = 'Codex';
10
+ command = 'codex';
11
+ historyRoot;
12
+ constructor(homeDirectory) {
13
+ super();
14
+ this.homeDirectory = homeDirectory;
15
+ this.historyRoot = path.join(homeDirectory, '.codex', 'sessions');
16
+ }
17
+ listSessions() {
18
+ return findFiles(this.historyRoot).flatMap((sourcePath) => {
19
+ const rows = readJsonLines(sourcePath).filter(isRecord);
20
+ const metadata = rows.find((row) => row.type === 'session_meta' || row.type === 'session_metadata');
21
+ const payload = isRecord(metadata?.payload) ? metadata.payload : metadata || {};
22
+ const id = asString(payload.id) || sessionIdFromFile(sourcePath);
23
+ const messages = rows.map(extractMessage).filter((message) => message !== undefined);
24
+ return [{
25
+ agent: this.id,
26
+ id,
27
+ title: sanitizeTitle(firstText(messages)) || '未命名 Codex 会话',
28
+ cwd: asString(payload.cwd) || process.cwd(),
29
+ updatedAt: asString(rows.at(-1)?.timestamp) || fs.statSync(sourcePath).mtime.toISOString(),
30
+ sourcePath,
31
+ }];
32
+ });
33
+ }
34
+ readTranscript(session) {
35
+ const messages = readJsonLines(session.sourcePath)
36
+ .map(extractMessage)
37
+ .filter((message) => message !== undefined);
38
+ return { source: session, messages, warnings: [] };
39
+ }
40
+ createResumeLaunch(session) {
41
+ return { command: [this.command, 'resume', session.id], cwd: session.cwd || process.cwd() };
42
+ }
43
+ createNewLaunch(input) {
44
+ const imageArgs = input.assetDirectory ? imageArguments(input.assetDirectory) : [];
45
+ const promptArgument = input.prompt ? [input.prompt] : [];
46
+ return { command: [this.command, ...imageArgs, ...promptArgument], cwd: input.cwd || process.cwd() };
47
+ }
48
+ deleteSession(session) {
49
+ this.execute(['delete', '--force', session.id], session.cwd);
50
+ }
51
+ integrationRoots(project) {
52
+ return [
53
+ { directory: path.join(this.homeDirectory, '.codex'), scope: 'user' },
54
+ ...(project ? [{ directory: path.join(project, '.codex'), scope: 'project' }] : []),
55
+ ];
56
+ }
57
+ listIntegrations(project) {
58
+ return [...super.listIntegrations(project), ...this.listPlugins(), ...this.listMcp()];
59
+ }
60
+ installPlugin(plugin) {
61
+ this.execute(['plugin', 'add', plugin]);
62
+ }
63
+ removeIntegration(item) {
64
+ if (item.kind === 'plugin') {
65
+ this.execute(['plugin', 'remove', item.name]);
66
+ return;
67
+ }
68
+ if (item.kind === 'mcp') {
69
+ this.execute(['mcp', 'remove', item.name]);
70
+ return;
71
+ }
72
+ this.removeLocalPath(item.location);
73
+ }
74
+ listPlugins() {
75
+ try {
76
+ return toCommandItems(this.execute(['plugin', 'list']), this.id, 'plugin');
77
+ }
78
+ catch {
79
+ return [];
80
+ }
81
+ }
82
+ listMcp() {
83
+ try {
84
+ return toCommandItems(this.execute(['mcp', 'list']), this.id, 'mcp');
85
+ }
86
+ catch {
87
+ return [];
88
+ }
89
+ }
90
+ }
91
+ function extractMessage(row) {
92
+ if (!isRecord(row))
93
+ return undefined;
94
+ const payload = isRecord(row.payload) ? row.payload : row;
95
+ const direct = normalizeMessage(payload.message || payload);
96
+ if (direct)
97
+ return direct;
98
+ if (payload.type === 'response_item' && isRecord(payload.payload))
99
+ return normalizeMessage(payload.payload);
100
+ return undefined;
101
+ }
102
+ function firstText(messages) {
103
+ const message = messages.find((item) => item.role === 'user');
104
+ const text = message?.content.find((content) => content.type === 'text');
105
+ return text?.type === 'text' ? text.text.replaceAll(/\s+/g, ' ').slice(0, 100) : undefined;
106
+ }
107
+ function sanitizeTitle(value) {
108
+ if (!value || /<environment_context>|# AGENTS\.md instructions|^\s*[\[{]/i.test(value)) {
109
+ return undefined;
110
+ }
111
+ return value;
112
+ }
113
+ function sessionIdFromFile(sourcePath) {
114
+ const match = path.basename(sourcePath).match(/([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i);
115
+ return match?.[1] || path.basename(sourcePath, '.jsonl');
116
+ }
117
+ function imageArguments(directory) {
118
+ if (!fs.existsSync(directory))
119
+ return [];
120
+ return fs.readdirSync(directory)
121
+ .filter((file) => /\.(png|jpe?g|gif|webp)$/i.test(file))
122
+ .flatMap((file) => ['--image', path.join(directory, file)]);
123
+ }
124
+ function toCommandItems(output, agent, kind) {
125
+ return output.split('\n')
126
+ .map((line) => line.trim().split(/\s+/)[0])
127
+ .filter((name) => Boolean(name) && !/^(name|no|installed)/i.test(name))
128
+ .map((name) => ({ agent, kind, name, scope: 'user', location: `codex ${kind}`, removable: true }));
129
+ }
130
+ function findFiles(directory) {
131
+ if (!fs.existsSync(directory))
132
+ return [];
133
+ return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
134
+ const entryPath = path.join(directory, entry.name);
135
+ if (entry.isDirectory())
136
+ return findFiles(entryPath);
137
+ return entry.isFile() && entry.name.endsWith('.jsonl') ? [entryPath] : [];
138
+ });
139
+ }
@@ -0,0 +1,26 @@
1
+ import fs from 'node:fs';
2
+ export function readJsonLines(filePath) {
3
+ try {
4
+ return fs.readFileSync(filePath, 'utf8')
5
+ .split('\n')
6
+ .flatMap((line) => {
7
+ if (!line.trim())
8
+ return [];
9
+ try {
10
+ return [JSON.parse(line)];
11
+ }
12
+ catch {
13
+ return [];
14
+ }
15
+ });
16
+ }
17
+ catch {
18
+ return [];
19
+ }
20
+ }
21
+ export function isRecord(value) {
22
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
23
+ }
24
+ export function asString(value) {
25
+ return typeof value === 'string' ? value : undefined;
26
+ }