@dungle-scrubs/tallow 0.8.7 → 0.8.8
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 +1 -1
- package/dist/cli.js +244 -54
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/sdk.d.ts +39 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +322 -13
- package/dist/sdk.js.map +1 -1
- package/dist/session-utils.d.ts +8 -0
- package/dist/session-utils.d.ts.map +1 -1
- package/dist/session-utils.js +38 -1
- package/dist/session-utils.js.map +1 -1
- package/extensions/__integration__/plan-rejection-feedback.test.ts +272 -0
- package/extensions/__integration__/worktree.test.ts +88 -0
- package/extensions/_icons/index.ts +2 -3
- package/extensions/_shared/inline-preview.ts +2 -4
- package/extensions/_shared/interop-events.ts +48 -0
- package/extensions/_shared/shell-policy.ts +2 -1
- package/extensions/_shared/tallow-paths.ts +48 -0
- package/extensions/agent-commands-tool/__tests__/parsing.test.ts +40 -1
- package/extensions/agent-commands-tool/index.ts +38 -6
- package/extensions/background-task-tool/index.ts +2 -2
- package/extensions/bash-tool-enhanced/index.ts +3 -4
- package/extensions/cd-tool/index.ts +3 -3
- package/extensions/claude-bridge/index.ts +2 -1
- package/extensions/command-prompt/__tests__/plugin-sources.test.ts +49 -0
- package/extensions/command-prompt/index.ts +36 -0
- package/extensions/context-files/index.ts +2 -1
- package/extensions/context-fork/index.ts +2 -1
- package/extensions/context-usage/__tests__/tool-result-memory.test.ts +62 -0
- package/extensions/context-usage/index.ts +169 -8
- package/extensions/debug/__tests__/analysis.test.ts +35 -2
- package/extensions/debug/__tests__/diagnostics-commands.test.ts +11 -2
- package/extensions/debug/analysis.ts +53 -16
- package/extensions/debug/index.ts +84 -10
- package/extensions/debug/logger.ts +2 -2
- package/extensions/health/index.ts +2 -2
- package/extensions/hooks/__tests__/claude-compat.test.ts +31 -0
- package/extensions/hooks/extension.json +3 -1
- package/extensions/hooks/hooks.schema.json +17 -1
- package/extensions/hooks/index.ts +52 -7
- package/extensions/lsp/index.ts +2 -7
- package/extensions/output-styles-tool/index.ts +2 -4
- package/extensions/plan-mode-tool/extension.json +1 -0
- package/extensions/plan-mode-tool/index.ts +119 -4
- package/extensions/prompt-suggestions/index.ts +2 -3
- package/extensions/random-spinner/index.ts +2 -3
- package/extensions/read-tool-enhanced/__tests__/notebook-read.test.ts +100 -0
- package/extensions/read-tool-enhanced/__tests__/notebook.test.ts +136 -0
- package/extensions/read-tool-enhanced/extension.json +4 -4
- package/extensions/read-tool-enhanced/index.ts +112 -10
- package/extensions/read-tool-enhanced/notebook.ts +526 -0
- package/extensions/rewind/__tests__/snapshots.test.ts +43 -1
- package/extensions/rewind/snapshots.ts +18 -6
- package/extensions/session-memory/index.ts +3 -2
- package/extensions/stats/stats-log.ts +2 -3
- package/extensions/subagent-tool/__tests__/discovery-defaults.test.ts +23 -0
- package/extensions/subagent-tool/__tests__/isolation-frontmatter.test.ts +100 -0
- package/extensions/subagent-tool/__tests__/model-router.test.ts +8 -0
- package/extensions/subagent-tool/agents.ts +77 -16
- package/extensions/subagent-tool/index.ts +213 -48
- package/extensions/subagent-tool/model-router.ts +3 -3
- package/extensions/subagent-tool/process.ts +233 -22
- package/extensions/subagent-tool/schema.ts +11 -0
- package/extensions/subagent-tool/widget.ts +15 -2
- package/extensions/tasks/__tests__/widget-subagents.test.ts +28 -7
- package/extensions/tasks/commands/register-tasks-extension.ts +15 -33
- package/extensions/tasks/state/index.ts +2 -2
- package/extensions/theme-selector/index.ts +3 -7
- package/extensions/worktree/__tests__/lifecycle.test.ts +115 -0
- package/extensions/worktree/extension.json +31 -0
- package/extensions/worktree/index.ts +149 -0
- package/extensions/worktree/lifecycle.ts +372 -0
- package/package.json +1 -1
- package/skills/tallow-expert/SKILL.md +1 -1
|
@@ -18,16 +18,16 @@ import {
|
|
|
18
18
|
unlinkSync,
|
|
19
19
|
watch,
|
|
20
20
|
} from "node:fs";
|
|
21
|
-
import { homedir } from "node:os";
|
|
22
21
|
import { join } from "node:path";
|
|
23
22
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
24
23
|
import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai";
|
|
25
24
|
import { atomicWriteFileSync } from "../../_shared/atomic-write.js";
|
|
25
|
+
import { getTallowPath } from "../../_shared/tallow-paths.js";
|
|
26
26
|
|
|
27
27
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
28
28
|
|
|
29
29
|
/** Directory root for team-based shared task lists. */
|
|
30
|
-
export const TEAMS_DIR =
|
|
30
|
+
export const TEAMS_DIR = getTallowPath("teams");
|
|
31
31
|
|
|
32
32
|
/** Max age for team directories before cleanup (7 days in ms). */
|
|
33
33
|
export const TEAM_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
3
|
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
6
4
|
import { Container, Key, matchesKey, Text } from "@mariozechner/pi-tui";
|
|
7
5
|
import { Type } from "@sinclair/typebox";
|
|
8
6
|
import { atomicWriteFileSync } from "../_shared/atomic-write.js";
|
|
7
|
+
import { getTallowSettingsPath } from "../_shared/tallow-paths.js";
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Theme switcher extension with live preview.
|
|
@@ -305,7 +304,7 @@ function filterByTags(themes: readonly ThemeEntry[], tags: string[]): readonly T
|
|
|
305
304
|
* @returns `false` (disabled), `true` (pick from all), or `string[]` (filter by tags)
|
|
306
305
|
*/
|
|
307
306
|
function readRandomThemeSetting(): false | true | string[] {
|
|
308
|
-
const settingsPath =
|
|
307
|
+
const settingsPath = getTallowSettingsPath();
|
|
309
308
|
try {
|
|
310
309
|
const raw = readFileSync(settingsPath, "utf-8");
|
|
311
310
|
const settings = JSON.parse(raw) as { randomThemeOnStart?: boolean | string[] };
|
|
@@ -319,10 +318,7 @@ function readRandomThemeSetting(): false | true | string[] {
|
|
|
319
318
|
* Persist theme selection to settings.json so it survives restarts.
|
|
320
319
|
*/
|
|
321
320
|
function persistTheme(themeName: string): void {
|
|
322
|
-
const
|
|
323
|
-
process.env.PI_CODING_AGENT_DIR ??
|
|
324
|
-
join(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".tallow");
|
|
325
|
-
const settingsPath = join(agentDir, "settings.json");
|
|
321
|
+
const settingsPath = getTallowSettingsPath();
|
|
326
322
|
try {
|
|
327
323
|
let settings: Record<string, unknown> = {};
|
|
328
324
|
if (existsSync(settingsPath)) {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
cleanupStaleWorktrees,
|
|
16
|
+
createWorktree,
|
|
17
|
+
removeWorktree,
|
|
18
|
+
TALLOW_WORKTREE_MARKER_FILE,
|
|
19
|
+
validateGitRepo,
|
|
20
|
+
} from "../lifecycle.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Run a git command and return trimmed stdout.
|
|
24
|
+
*
|
|
25
|
+
* @param cwd - Working directory
|
|
26
|
+
* @param args - Git arguments
|
|
27
|
+
* @returns Trimmed stdout
|
|
28
|
+
*/
|
|
29
|
+
function git(cwd: string, ...args: string[]): string {
|
|
30
|
+
const output = execFileSync("git", args, {
|
|
31
|
+
cwd,
|
|
32
|
+
encoding: "utf-8",
|
|
33
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
34
|
+
});
|
|
35
|
+
return output.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a temporary git repository with an initial commit.
|
|
40
|
+
*
|
|
41
|
+
* @returns Repo root path
|
|
42
|
+
*/
|
|
43
|
+
function createTempRepo(): string {
|
|
44
|
+
const repo = mkdtempSync(join(tmpdir(), "tallow-worktree-test-"));
|
|
45
|
+
git(repo, "init");
|
|
46
|
+
git(repo, "config", "user.email", "test@example.com");
|
|
47
|
+
git(repo, "config", "user.name", "Test User");
|
|
48
|
+
writeFileSync(join(repo, "README.md"), "# test\n", "utf-8");
|
|
49
|
+
git(repo, "add", "README.md");
|
|
50
|
+
git(repo, "commit", "-m", "init");
|
|
51
|
+
return repo;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("worktree lifecycle", () => {
|
|
55
|
+
let repoRoot = "";
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
repoRoot = createTempRepo();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
rmSync(repoRoot, { force: true, recursive: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("creates a detached worktree inside a git repo", () => {
|
|
66
|
+
const created = createWorktree(repoRoot, { scope: "subagent" });
|
|
67
|
+
expect(existsSync(created.worktreePath)).toBe(true);
|
|
68
|
+
expect(created.scope).toBe("subagent");
|
|
69
|
+
const markerPath = join(created.worktreePath, TALLOW_WORKTREE_MARKER_FILE);
|
|
70
|
+
expect(existsSync(markerPath)).toBe(true);
|
|
71
|
+
removeWorktree(created.worktreePath);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("fails validation outside git repositories", () => {
|
|
75
|
+
const outside = mkdtempSync(join(tmpdir(), "tallow-worktree-nogit-"));
|
|
76
|
+
try {
|
|
77
|
+
expect(() => validateGitRepo(outside)).toThrow(
|
|
78
|
+
/not inside a git repository|not a git repository/i
|
|
79
|
+
);
|
|
80
|
+
} finally {
|
|
81
|
+
rmSync(outside, { force: true, recursive: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("removes worktrees and is idempotent for missing paths", () => {
|
|
86
|
+
const created = createWorktree(repoRoot, { scope: "session" });
|
|
87
|
+
const first = removeWorktree(created.worktreePath);
|
|
88
|
+
expect(first.removed).toBe(true);
|
|
89
|
+
expect(existsSync(created.worktreePath)).toBe(false);
|
|
90
|
+
|
|
91
|
+
const second = removeWorktree(created.worktreePath);
|
|
92
|
+
expect(second.method).toBe("none");
|
|
93
|
+
expect(second.removed).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("resolves repository root from nested directories", () => {
|
|
97
|
+
const nested = join(repoRoot, "nested", "child");
|
|
98
|
+
mkdirSync(nested, { recursive: true });
|
|
99
|
+
const root = validateGitRepo(nested).repoRoot;
|
|
100
|
+
expect(realpathSync(root)).toBe(realpathSync(repoRoot));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("prunes stale managed worktrees whose owner pid is gone", () => {
|
|
104
|
+
const created = createWorktree(repoRoot, { scope: "session" });
|
|
105
|
+
const markerPath = join(created.worktreePath, TALLOW_WORKTREE_MARKER_FILE);
|
|
106
|
+
const marker = JSON.parse(readFileSync(markerPath, "utf-8")) as Record<string, unknown>;
|
|
107
|
+
marker.pid = 999_999_999;
|
|
108
|
+
writeFileSync(markerPath, `${JSON.stringify(marker, null, 2)}\n`, "utf-8");
|
|
109
|
+
|
|
110
|
+
const cleaned = cleanupStaleWorktrees(repoRoot);
|
|
111
|
+
expect(cleaned.scannedCount).toBeGreaterThan(0);
|
|
112
|
+
expect(cleaned.removedCount).toBeGreaterThanOrEqual(1);
|
|
113
|
+
expect(existsSync(created.worktreePath)).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "worktree",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Managed git worktree isolation lifecycle for sessions and subagents.",
|
|
5
|
+
"whenToUse": "Use when tallow should run in temporary detached git worktrees with reliable cleanup and lifecycle hooks.",
|
|
6
|
+
"capabilities": {
|
|
7
|
+
"events": ["before_agent_start", "session_shutdown", "session_start"]
|
|
8
|
+
},
|
|
9
|
+
"permissionSurface": {
|
|
10
|
+
"filesystem": "write",
|
|
11
|
+
"shell": false,
|
|
12
|
+
"network": false,
|
|
13
|
+
"subprocess": true
|
|
14
|
+
},
|
|
15
|
+
"category": "integration",
|
|
16
|
+
"tags": ["worktree", "isolation", "git", "cleanup"],
|
|
17
|
+
"files": ["index.ts", "lifecycle.ts"],
|
|
18
|
+
"relationships": [
|
|
19
|
+
{
|
|
20
|
+
"name": "subagent-tool",
|
|
21
|
+
"kind": "enhances",
|
|
22
|
+
"reason": "Subagent worktree isolation uses the shared lifecycle primitives"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "hooks",
|
|
26
|
+
"kind": "enhances",
|
|
27
|
+
"reason": "Emits worktree lifecycle events for hook automation"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"npmDependencies": {}
|
|
31
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
emitWorktreeLifecycleEvent,
|
|
4
|
+
type WorktreeLifecycleEventPayload,
|
|
5
|
+
} from "../_shared/interop-events.js";
|
|
6
|
+
import { cleanupStaleWorktrees, removeWorktree, validateGitRepo } from "./lifecycle.js";
|
|
7
|
+
|
|
8
|
+
/** Env var containing active session worktree path (set by CLI when -w/--worktree is used). */
|
|
9
|
+
const TALLOW_WORKTREE_PATH_ENV = "TALLOW_WORKTREE_PATH";
|
|
10
|
+
|
|
11
|
+
/** Env var containing original cwd before session worktree activation. */
|
|
12
|
+
const TALLOW_WORKTREE_ORIGINAL_CWD_ENV = "TALLOW_WORKTREE_ORIGINAL_CWD";
|
|
13
|
+
|
|
14
|
+
/** Marker added to system prompt so worktree context is only injected once. */
|
|
15
|
+
const SESSION_WORKTREE_PROMPT_MARKER = "Session worktree isolation is active.";
|
|
16
|
+
|
|
17
|
+
/** In-memory state for session-level worktree lifecycle management. */
|
|
18
|
+
interface SessionWorktreeState {
|
|
19
|
+
readonly originalCwd: string;
|
|
20
|
+
readonly repoRoot: string;
|
|
21
|
+
readonly worktreePath: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Worktree lifecycle extension.
|
|
26
|
+
*
|
|
27
|
+
* Responsibilities:
|
|
28
|
+
* - prune stale managed worktrees on session startup
|
|
29
|
+
* - emit session-level worktree lifecycle events for hooks
|
|
30
|
+
* - inject explicit worktree context into the system prompt
|
|
31
|
+
* - perform best-effort teardown on session shutdown
|
|
32
|
+
*
|
|
33
|
+
* @param pi - Extension API
|
|
34
|
+
*/
|
|
35
|
+
export default function worktreeExtension(pi: ExtensionAPI): void {
|
|
36
|
+
let sessionState: SessionWorktreeState | undefined;
|
|
37
|
+
let cleanedUp = false;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Best-effort session-worktree cleanup routine.
|
|
41
|
+
*
|
|
42
|
+
* @param reason - Cleanup trigger reason for diagnostics
|
|
43
|
+
*/
|
|
44
|
+
const cleanupSessionWorktree = (reason: "session_shutdown"): void => {
|
|
45
|
+
if (cleanedUp || !sessionState) return;
|
|
46
|
+
cleanedUp = true;
|
|
47
|
+
removeWorktree(sessionState.worktreePath);
|
|
48
|
+
emitWorktreeLifecycleEvent(pi.events, "worktree_remove", {
|
|
49
|
+
agentId: undefined,
|
|
50
|
+
repoRoot: sessionState.repoRoot,
|
|
51
|
+
scope: "session",
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
worktreePath: sessionState.worktreePath,
|
|
54
|
+
});
|
|
55
|
+
if (reason === "session_shutdown") {
|
|
56
|
+
delete process.env[TALLOW_WORKTREE_PATH_ENV];
|
|
57
|
+
delete process.env[TALLOW_WORKTREE_ORIGINAL_CWD_ENV];
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
62
|
+
const cleanupRoot = resolveCleanupRoot(ctx.cwd);
|
|
63
|
+
if (cleanupRoot) {
|
|
64
|
+
try {
|
|
65
|
+
cleanupStaleWorktrees(cleanupRoot);
|
|
66
|
+
} catch {
|
|
67
|
+
// Startup stale cleanup is best-effort.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
sessionState = resolveSessionWorktreeState(ctx.cwd);
|
|
72
|
+
cleanedUp = false;
|
|
73
|
+
if (!sessionState) return;
|
|
74
|
+
|
|
75
|
+
const payload: WorktreeLifecycleEventPayload = {
|
|
76
|
+
agentId: undefined,
|
|
77
|
+
repoRoot: sessionState.repoRoot,
|
|
78
|
+
scope: "session",
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
worktreePath: sessionState.worktreePath,
|
|
81
|
+
};
|
|
82
|
+
emitWorktreeLifecycleEvent(pi.events, "worktree_create", payload);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
pi.on("before_agent_start", async (event) => {
|
|
86
|
+
if (!sessionState) return;
|
|
87
|
+
if (event.systemPrompt.includes(SESSION_WORKTREE_PROMPT_MARKER)) return;
|
|
88
|
+
|
|
89
|
+
const hint = [
|
|
90
|
+
SESSION_WORKTREE_PROMPT_MARKER,
|
|
91
|
+
`Current cwd is an isolated detached git worktree: ${sessionState.worktreePath}`,
|
|
92
|
+
`Original cwd before isolation: ${sessionState.originalCwd}`,
|
|
93
|
+
"Any file edits happen inside this temporary worktree.",
|
|
94
|
+
].join("\n");
|
|
95
|
+
return {
|
|
96
|
+
systemPrompt: `${event.systemPrompt}\n\n${hint}`,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
pi.on("session_shutdown", async () => {
|
|
101
|
+
cleanupSessionWorktree("session_shutdown");
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve cleanup root for startup stale-worktree pruning.
|
|
107
|
+
*
|
|
108
|
+
* @param cwd - Current session cwd
|
|
109
|
+
* @returns Repository root when available
|
|
110
|
+
*/
|
|
111
|
+
function resolveCleanupRoot(cwd: string): string | undefined {
|
|
112
|
+
const originalCwd = process.env[TALLOW_WORKTREE_ORIGINAL_CWD_ENV];
|
|
113
|
+
if (originalCwd) {
|
|
114
|
+
try {
|
|
115
|
+
return validateGitRepo(originalCwd).repoRoot;
|
|
116
|
+
} catch {
|
|
117
|
+
// Fall through to session cwd.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
return validateGitRepo(cwd).repoRoot;
|
|
123
|
+
} catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build session worktree state from environment metadata.
|
|
130
|
+
*
|
|
131
|
+
* @param fallbackCwd - Session cwd fallback when original cwd metadata is absent
|
|
132
|
+
* @returns Parsed session worktree state when metadata is valid
|
|
133
|
+
*/
|
|
134
|
+
function resolveSessionWorktreeState(fallbackCwd: string): SessionWorktreeState | undefined {
|
|
135
|
+
const worktreePath = process.env[TALLOW_WORKTREE_PATH_ENV];
|
|
136
|
+
if (!worktreePath) return undefined;
|
|
137
|
+
const originalCwd = process.env[TALLOW_WORKTREE_ORIGINAL_CWD_ENV] ?? fallbackCwd;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const repoRoot = validateGitRepo(originalCwd).repoRoot;
|
|
141
|
+
return {
|
|
142
|
+
originalCwd,
|
|
143
|
+
repoRoot,
|
|
144
|
+
worktreePath,
|
|
145
|
+
};
|
|
146
|
+
} catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|