ultracontext 1.4.13 → 1.6.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.
- package/dist/cli/entry.mjs +9 -3
- package/dist/cli/entry.mjs.map +1 -1
- package/dist/cli/onboarding.mjs +268 -111
- package/dist/cli/onboarding.mjs.map +1 -1
- package/dist/cli/sdk-sync.mjs +199 -939
- package/dist/cli/sdk-sync.mjs.map +1 -1
- package/dist/cli/switch.mjs +168 -0
- package/dist/cli/switch.mjs.map +1 -0
- package/dist/{ctl-CXfNEPN8.mjs → ctl-DTQZxn3N.mjs} +2 -2
- package/dist/{ctl-CXfNEPN8.mjs.map → ctl-DTQZxn3N.mjs.map} +1 -1
- package/dist/hero-art-C03HmDXN.mjs +46 -0
- package/dist/hero-art-C03HmDXN.mjs.map +1 -0
- package/dist/index.d.mts +21 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +25 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{launcher-BMMjzr5k.mjs → launcher-ZylswrpR.mjs} +3 -3
- package/dist/{launcher-BMMjzr5k.mjs.map → launcher-ZylswrpR.mjs.map} +1 -1
- package/dist/{lock-5aJnda81.mjs → lock-BhZX2aF3.mjs} +2 -2
- package/dist/{lock-5aJnda81.mjs.map → lock-BhZX2aF3.mjs.map} +1 -1
- package/dist/onboarding-preferences-Alhblobi.mjs +76 -0
- package/dist/onboarding-preferences-Alhblobi.mjs.map +1 -0
- package/dist/src-Bovo1ukU.mjs +1200 -0
- package/dist/src-Bovo1ukU.mjs.map +1 -0
- package/dist/{tui-DZ1SDOH2.mjs → tui-DLEjew3K.mjs} +334 -115
- package/dist/tui-DLEjew3K.mjs.map +1 -0
- package/dist/utils-BTfShW0g.mjs +36 -0
- package/dist/utils-BTfShW0g.mjs.map +1 -0
- package/dist/{utils-CmuIYHtm.mjs → utils-D9CKnbke.mjs} +26 -34
- package/dist/utils-D9CKnbke.mjs.map +1 -0
- package/lib/register-skills.mjs +96 -0
- package/package.json +8 -3
- package/plugin/.claude-plugin/plugin.json +6 -0
- package/plugin/README.md +112 -0
- package/plugin/marketplace.json +17 -0
- package/plugin/skills/switch/SKILL.md +27 -0
- package/postinstall.mjs +35 -2
- package/dist/Spinner-CwBjkXHv.mjs +0 -153
- package/dist/Spinner-CwBjkXHv.mjs.map +0 -1
- package/dist/tui-DZ1SDOH2.mjs.map +0 -1
- package/dist/utils-CmuIYHtm.mjs.map +0 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import os from "node:os";
|
|
3
|
-
import crypto from "node:crypto";
|
|
4
3
|
|
|
5
4
|
//#region ../../packages/parsers/src/utils.mjs
|
|
6
5
|
function expandHome(inputPath) {
|
|
@@ -9,6 +8,15 @@ function expandHome(inputPath) {
|
|
|
9
8
|
if (inputPath.startsWith("~/")) return path.join(os.homedir(), inputPath.slice(2));
|
|
10
9
|
return inputPath;
|
|
11
10
|
}
|
|
11
|
+
function claudeProjectDirName(cwd) {
|
|
12
|
+
return path.resolve(String(cwd || process.cwd())).replace(/[\\/]/g, "-").replace(/[^A-Za-z0-9._-]/g, "-");
|
|
13
|
+
}
|
|
14
|
+
function isSafeCwd(value) {
|
|
15
|
+
if (typeof value !== "string" || !value.length) return false;
|
|
16
|
+
if (!path.isAbsolute(value)) return false;
|
|
17
|
+
if (/[\x00-\x1F\x7F\u0085\u2028\u2029]/.test(value)) return false;
|
|
18
|
+
return path.normalize(value) === value;
|
|
19
|
+
}
|
|
12
20
|
function truncateString(value, maxLen = 4e3) {
|
|
13
21
|
if (typeof value !== "string") return value;
|
|
14
22
|
if (value.length <= maxLen) return value;
|
|
@@ -68,39 +76,23 @@ function asIso(value) {
|
|
|
68
76
|
if (Number.isNaN(d.getTime())) return (/* @__PURE__ */ new Date()).toISOString();
|
|
69
77
|
return d.toISOString();
|
|
70
78
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
function
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"true",
|
|
87
|
-
"yes",
|
|
88
|
-
"on"
|
|
89
|
-
].includes(normalized)) return true;
|
|
90
|
-
if ([
|
|
91
|
-
"0",
|
|
92
|
-
"false",
|
|
93
|
-
"no",
|
|
94
|
-
"off"
|
|
95
|
-
].includes(normalized)) return false;
|
|
96
|
-
return fallback;
|
|
97
|
-
}
|
|
98
|
-
function extractProjectPathFromFile(filePath) {
|
|
99
|
-
const match = filePath.match(/\.claude\/projects\/([^/]+)/);
|
|
100
|
-
if (!match) return null;
|
|
101
|
-
return match[1].replace(/-/g, "/");
|
|
79
|
+
const IDE_TAG_RE = /<ide_[^>]*>[\s\S]*?<\/ide_[^>]*>/g;
|
|
80
|
+
const SYSTEM_TAG_RES = [
|
|
81
|
+
/<local-command-caveat[^>]*>[\s\S]*?<\/local-command-caveat>/g,
|
|
82
|
+
/<system-reminder[^>]*>[\s\S]*?<\/system-reminder>/g,
|
|
83
|
+
/<command-name[^>]*>[\s\S]*?<\/command-name>/g,
|
|
84
|
+
/<command-message[^>]*>[\s\S]*?<\/command-message>/g,
|
|
85
|
+
/<command-args[^>]*>[\s\S]*?<\/command-args>/g,
|
|
86
|
+
/<local-command-stdout[^>]*>[\s\S]*?<\/local-command-stdout>/g,
|
|
87
|
+
/<\/?user_query>/g
|
|
88
|
+
];
|
|
89
|
+
function stripIDEContextTags(text) {
|
|
90
|
+
if (!text) return "";
|
|
91
|
+
let result = text.replace(IDE_TAG_RE, "");
|
|
92
|
+
for (const re of SYSTEM_TAG_RES) result = result.replace(re, "");
|
|
93
|
+
return result.replace(/\n{3,}/g, "\n\n").trim();
|
|
102
94
|
}
|
|
103
95
|
|
|
104
96
|
//#endregion
|
|
105
|
-
export {
|
|
106
|
-
//# sourceMappingURL=utils-
|
|
97
|
+
export { extractSessionIdFromPath as a, normalizeRole as c, safeJsonParse as d, stripIDEContextTags as f, expandHome as i, normalizeWhitespace as l, truncateString as m, claudeProjectDirName as n, firstMessageTimestamp as o, toMessage as p, coerceMessageText as r, isSafeCwd as s, asIso as t, preserveText as u };
|
|
98
|
+
//# sourceMappingURL=utils-D9CKnbke.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils-D9CKnbke.mjs","names":[],"sources":["../../../packages/parsers/src/utils.mjs"],"sourcesContent":["import os from \"node:os\";\nimport path from \"node:path\";\n\n// resolve ~ to home directory\nexport function expandHome(inputPath) {\n if (!inputPath || !inputPath.startsWith(\"~\")) return inputPath;\n if (inputPath === \"~\") return os.homedir();\n if (inputPath.startsWith(\"~/\")) return path.join(os.homedir(), inputPath.slice(2));\n return inputPath;\n}\n\n// Claude Code project directory name from cwd (shared by writer + switch)\nexport function claudeProjectDirName(cwd) {\n const resolved = path.resolve(String(cwd || process.cwd()));\n return resolved.replace(/[\\\\/]/g, \"-\").replace(/[^A-Za-z0-9._-]/g, \"-\");\n}\n\n// accept only absolute paths in canonical form with no control chars or unicode\n// line separators — hardens cwd used in shell/AppleScript sinks. Rejects \"..\"\n// traversal and encodings that would round-trip unsafely through a terminal emulator.\nexport function isSafeCwd(value) {\n if (typeof value !== \"string\" || !value.length) return false;\n if (!path.isAbsolute(value)) return false;\n // reject all C0 controls, DEL, Unicode NEL (U+0085), LINE SEP (U+2028), PARA SEP (U+2029)\n if (/[\\x00-\\x1F\\x7F\\u0085\\u2028\\u2029]/.test(value)) return false;\n // require canonical form — path.normalize strips ``..``, `.`, and redundant slashes;\n // if input differs, reject rather than silently normalize behind the caller\n return path.normalize(value) === value;\n}\n\n// safe truncation with indicator\nexport function truncateString(value, maxLen = 4000) {\n if (typeof value !== \"string\") return value;\n if (value.length <= maxLen) return value;\n return `${value.slice(0, maxLen)}... [truncated ${value.length - maxLen} chars]`;\n}\n\n// swallow malformed JSON\nexport function safeJsonParse(line) {\n try {\n return JSON.parse(line);\n } catch {\n return null;\n }\n}\n\n// pull UUID from file path, fall back to filename\nexport function extractSessionIdFromPath(filePath) {\n const uuidMatch = filePath.match(\n /([0-9a-f]{8}-[0-9a-f]{4,}-[0-9a-f]{4,}-[0-9a-f]{4,}-[0-9a-f]{8,})/i\n );\n if (uuidMatch) return uuidMatch[1];\n\n const fileName = path.basename(filePath, \".jsonl\");\n return fileName || \"unknown-session\";\n}\n\n// normalize role strings across agents\nexport function normalizeRole(role, fallback = \"system\") {\n const lowered = String(role ?? \"\").toLowerCase();\n if ([\"user\", \"human\"].includes(lowered)) return \"user\";\n if ([\"assistant\", \"agent\", \"ai\"].includes(lowered)) return \"assistant\";\n return fallback;\n}\n\n// collapse whitespace to single spaces\nexport function normalizeWhitespace(value) {\n return String(value ?? \"\").replace(/\\s+/g, \" \").trim();\n}\n\n// preserve newlines, trim lines, collapse 3+ blank lines → 2\nexport function preserveText(value) {\n return String(value ?? \"\")\n .split(\"\\n\")\n .map((l) => l.trimEnd())\n .join(\"\\n\")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n}\n\n// coerce message content to string\nexport function toMessage(raw, maxLen = 12000) {\n if (!raw) return \"\";\n if (typeof raw === \"string\") return truncateString(raw, maxLen);\n if (typeof raw === \"object\") return truncateString(JSON.stringify(raw), maxLen);\n return truncateString(String(raw), maxLen);\n}\n\n// coerce message content to plain text\nexport function coerceMessageText(message) {\n const content = message?.content;\n if (typeof content === \"string\") return content;\n if (content && typeof content === \"object\") {\n if (typeof content.message === \"string\") return content.message;\n if (typeof content.text === \"string\") return content.text;\n if (typeof content.raw === \"string\") return content.raw;\n }\n if (typeof message?.message === \"string\") return message.message;\n return \"\";\n}\n\n// get timestamp from first message\nexport function firstMessageTimestamp(messages) {\n return (\n messages?.[0]?.content?.timestamp ??\n messages?.[0]?.metadata?.timestamp ??\n new Date().toISOString()\n );\n}\n\n// coerce ISO timestamp, fall back to now\nexport function asIso(value) {\n if (!value) return new Date().toISOString();\n const d = new Date(String(value));\n if (Number.isNaN(d.getTime())) return new Date().toISOString();\n return d.toISOString();\n}\n\n// strip IDE-injected context tags from user prompts\nconst IDE_TAG_RE = /<ide_[^>]*>[\\s\\S]*?<\\/ide_[^>]*>/g;\nconst SYSTEM_TAG_RES = [\n /<local-command-caveat[^>]*>[\\s\\S]*?<\\/local-command-caveat>/g,\n /<system-reminder[^>]*>[\\s\\S]*?<\\/system-reminder>/g,\n /<command-name[^>]*>[\\s\\S]*?<\\/command-name>/g,\n /<command-message[^>]*>[\\s\\S]*?<\\/command-message>/g,\n /<command-args[^>]*>[\\s\\S]*?<\\/command-args>/g,\n /<local-command-stdout[^>]*>[\\s\\S]*?<\\/local-command-stdout>/g,\n /<\\/?user_query>/g,\n];\n\nexport function stripIDEContextTags(text) {\n if (!text) return \"\";\n let result = text.replace(IDE_TAG_RE, \"\");\n for (const re of SYSTEM_TAG_RES) {\n result = result.replace(re, \"\");\n }\n return result.replace(/\\n{3,}/g, \"\\n\\n\").trim();\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Register plugin skills into agent skill dirs (~/.claude/skills, ~/.codex/skills).
|
|
2
|
+
// Exported as a pure function so it's testable without running npm install.
|
|
3
|
+
//
|
|
4
|
+
// Contract:
|
|
5
|
+
// - Walk pluginDir/<name>/SKILL.md, validate <name> against SAFE_SKILL_NAME.
|
|
6
|
+
// - For each agent dir, for each skill:
|
|
7
|
+
// - If target SKILL.md is a symlink → skip (don't follow, could escape into user files).
|
|
8
|
+
// - If target exists as regular file:
|
|
9
|
+
// - If sidecar .ultracontext-version present with matching version → no-op (already up-to-date).
|
|
10
|
+
// - If sidecar present but version differs → upgrade (replace + bump sidecar).
|
|
11
|
+
// - If sidecar missing → preserve (assume user customization).
|
|
12
|
+
// - If target missing → create with COPYFILE_EXCL + write sidecar.
|
|
13
|
+
// - All fs errors are swallowed per-agent-dir so CI / read-only installs don't fail npm.
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
// skill dir name must be a single path segment — defend against escape via crafted tarball
|
|
19
|
+
const SAFE_SKILL_NAME = /^[A-Za-z0-9._-]+$/;
|
|
20
|
+
|
|
21
|
+
const VERSION_MARKER = ".ultracontext-version";
|
|
22
|
+
|
|
23
|
+
// read version marker or return null if absent / unreadable
|
|
24
|
+
function readMarker(markerFile) {
|
|
25
|
+
try {
|
|
26
|
+
return fs.readFileSync(markerFile, "utf8").trim();
|
|
27
|
+
} catch { return null; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// attempt to register one skill into one agent dir; swallow errors
|
|
31
|
+
function registerOne({ sourceFile, targetDir, targetFile, markerFile, packageVersion, logger }) {
|
|
32
|
+
try {
|
|
33
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
34
|
+
|
|
35
|
+
// lstat (not stat) so symlinks are detected and never followed
|
|
36
|
+
let existing = null;
|
|
37
|
+
try { existing = fs.lstatSync(targetFile); } catch { /* missing */ }
|
|
38
|
+
|
|
39
|
+
if (existing) {
|
|
40
|
+
if (existing.isSymbolicLink()) {
|
|
41
|
+
logger?.(`skipping ${targetFile} (symlink — preserving)`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const marker = readMarker(markerFile);
|
|
45
|
+
if (marker === null) {
|
|
46
|
+
logger?.(`skipping ${targetFile} (no marker — preserving user edit)`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (marker === packageVersion) {
|
|
50
|
+
// already at current version — no-op
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// managed-but-stale: remove + re-copy
|
|
54
|
+
fs.unlinkSync(targetFile);
|
|
55
|
+
logger?.(`upgrading ${targetFile} from v${marker} to v${packageVersion}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// atomic create — fails loudly on EEXIST so we never clobber by accident
|
|
59
|
+
fs.copyFileSync(sourceFile, targetFile, fs.constants.COPYFILE_EXCL);
|
|
60
|
+
fs.writeFileSync(markerFile, packageVersion);
|
|
61
|
+
if (!existing) logger?.(`registered ${targetFile} (v${packageVersion})`);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// read-only fs, EACCES in CI, tarball race — never fail the install
|
|
64
|
+
logger?.(`skip ${targetFile} (${err?.code || "error"})`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function registerSkills({ pluginDir, agentDirs, packageVersion, logger }) {
|
|
69
|
+
let skillNames;
|
|
70
|
+
try {
|
|
71
|
+
skillNames = fs.readdirSync(pluginDir).filter((name) => {
|
|
72
|
+
if (!SAFE_SKILL_NAME.test(name)) return false;
|
|
73
|
+
try { return fs.statSync(join(pluginDir, name)).isDirectory(); } catch { return false; }
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
// plugin dir missing (shouldn't happen in a real install)
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const skillName of skillNames) {
|
|
81
|
+
const sourceFile = join(pluginDir, skillName, "SKILL.md");
|
|
82
|
+
try { fs.statSync(sourceFile); } catch { continue; }
|
|
83
|
+
|
|
84
|
+
for (const agentSkillsDir of agentDirs) {
|
|
85
|
+
const targetDir = join(agentSkillsDir, skillName);
|
|
86
|
+
registerOne({
|
|
87
|
+
sourceFile,
|
|
88
|
+
targetDir,
|
|
89
|
+
targetFile: join(targetDir, "SKILL.md"),
|
|
90
|
+
markerFile: join(targetDir, VERSION_MARKER),
|
|
91
|
+
packageVersion,
|
|
92
|
+
logger,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "ultracontext",
|
|
3
3
|
"description": "JavaScript/TypeScript SDK + CLI for the UltraContext API",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.6.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.12.0"
|
|
8
8
|
},
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"build": "tsdown",
|
|
14
14
|
"dev": "tsdown --watch",
|
|
15
15
|
"prepack": "pnpm run build",
|
|
16
|
-
"postinstall": "node postinstall.mjs"
|
|
16
|
+
"postinstall": "node postinstall.mjs",
|
|
17
|
+
"test": "node --test tests/**/*.test.mjs"
|
|
17
18
|
},
|
|
18
19
|
"main": "./dist/index.mjs",
|
|
19
20
|
"types": "./dist/index.d.mts",
|
|
@@ -56,9 +57,11 @@
|
|
|
56
57
|
},
|
|
57
58
|
"files": [
|
|
58
59
|
"dist/",
|
|
60
|
+
"lib/",
|
|
59
61
|
"ultracontext.mjs",
|
|
60
62
|
"postinstall.mjs",
|
|
61
|
-
"assets/"
|
|
63
|
+
"assets/",
|
|
64
|
+
"plugin/"
|
|
62
65
|
],
|
|
63
66
|
"dependencies": {
|
|
64
67
|
"@mishieck/ink-titled-box": "^0.4.2",
|
|
@@ -68,7 +71,9 @@
|
|
|
68
71
|
"react": "^19.2.4"
|
|
69
72
|
},
|
|
70
73
|
"devDependencies": {
|
|
74
|
+
"@ultracontext/parsers": "workspace:*",
|
|
71
75
|
"@ultracontext/sync": "workspace:*",
|
|
76
|
+
"node-pty": "^1.1.0",
|
|
72
77
|
"tsdown": "^0.20.3",
|
|
73
78
|
"typescript": "^5.9.3"
|
|
74
79
|
},
|
package/plugin/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# UltraContext Plugin for Claude Code
|
|
2
|
+
|
|
3
|
+
Cross-agent session portability. Switch your Claude Code conversation to Codex (or back) with full context, one command.
|
|
4
|
+
|
|
5
|
+
## Install — 10 seconds
|
|
6
|
+
|
|
7
|
+
Open Claude Code and paste this:
|
|
8
|
+
|
|
9
|
+
> Install the UltraContext plugin: run **`npm i -g ultracontext && git clone --single-branch --depth 1 https://github.com/ultracontext/ultracontext.git /tmp/uc-plugin && mkdir -p ~/.claude/skills/switch && cp /tmp/uc-plugin/apps/js-sdk/plugin/skills/switch/SKILL.md ~/.claude/skills/switch/SKILL.md && rm -rf /tmp/uc-plugin`** then add to CLAUDE.md that `/switch codex` switches the current session to Codex with full context. Powered by UltraContext.
|
|
10
|
+
|
|
11
|
+
That's it. Type `/switch codex` to try it.
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
You're deep in a Claude Code session. You want to continue in Codex. Type `/switch codex`. UltraContext reads your session JSONL from disk, converts it to Codex's native format, writes it to `~/.codex/sessions/`, and opens Codex with your full conversation.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Claude Code session Codex session
|
|
19
|
+
~/.claude/projects/.../*.jsonl → ~/.codex/sessions/.../*.jsonl
|
|
20
|
+
333 messages 333 messages
|
|
21
|
+
full context full context
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
No API calls. No copy-paste. Pure local file conversion.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
/switch codex Switch to Codex with full session
|
|
30
|
+
/switch codex --last 50 Only carry last 50 messages
|
|
31
|
+
/switch codex --no-launch Write session file, don't open Codex
|
|
32
|
+
/switch claude Switch from Codex back to Claude
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or from any terminal:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
ultracontext switch codex
|
|
39
|
+
ultracontext switch codex --last 50
|
|
40
|
+
ultracontext switch codex --no-launch
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## How it works
|
|
44
|
+
|
|
45
|
+
UltraContext already has parsers that read Claude/Codex/Cursor/Gemini sessions, and writers that output native formats. `/switch` connects them:
|
|
46
|
+
|
|
47
|
+
1. Reads your current session JSONL from disk
|
|
48
|
+
2. Parses with the source agent's parser (`parseClaudeCodeLine`)
|
|
49
|
+
3. Filters to user/assistant messages (strips system noise)
|
|
50
|
+
4. Writes native format via the target writer (`writeCodexSession`)
|
|
51
|
+
5. Opens target agent in a new terminal tab
|
|
52
|
+
|
|
53
|
+
Supports: Ghostty, iTerm2, Terminal.app. Other terminals: prints the command to run.
|
|
54
|
+
|
|
55
|
+
## Alternative install methods
|
|
56
|
+
|
|
57
|
+
### Standalone skill (no npm required)
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
mkdir -p ~/.claude/skills/switch
|
|
61
|
+
curl -sL https://raw.githubusercontent.com/ultracontext/ultracontext/main/apps/js-sdk/plugin/skills/switch/SKILL.md > ~/.claude/skills/switch/SKILL.md
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then install the CLI: `npm i -g ultracontext`
|
|
65
|
+
|
|
66
|
+
### Plugin mode (for marketplace)
|
|
67
|
+
|
|
68
|
+
Add to your Claude Code `settings.json`:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"extraKnownMarketplaces": {
|
|
73
|
+
"ultracontext": {
|
|
74
|
+
"source": {
|
|
75
|
+
"source": "github",
|
|
76
|
+
"repo": "ultracontext/ultracontext",
|
|
77
|
+
"path": "apps/js-sdk/plugin"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"enabledPlugins": {
|
|
82
|
+
"ultracontext@ultracontext": true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This gives you `/ultracontext:switch` as a namespaced command.
|
|
88
|
+
|
|
89
|
+
### CLI only (no plugin)
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm i -g ultracontext
|
|
93
|
+
ultracontext switch codex
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Works from any terminal. No Claude Code plugin needed.
|
|
97
|
+
|
|
98
|
+
## Supported agents
|
|
99
|
+
|
|
100
|
+
| Direction | Status |
|
|
101
|
+
|---|---|
|
|
102
|
+
| Claude → Codex | ✓ Tested |
|
|
103
|
+
| Codex → Claude | ✓ Tested |
|
|
104
|
+
| Claude → Cursor | Coming soon |
|
|
105
|
+
| Claude → Gemini | Coming soon |
|
|
106
|
+
| Codex → Cursor | Coming soon |
|
|
107
|
+
|
|
108
|
+
## Requirements
|
|
109
|
+
|
|
110
|
+
- [ultracontext](https://www.npmjs.com/package/ultracontext) CLI (`npm i -g ultracontext`)
|
|
111
|
+
- Claude Code or Codex CLI installed
|
|
112
|
+
- macOS, Linux, or Windows
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "ultracontext",
|
|
4
|
+
"description": "Cross-agent session portability. Powered by UltraContext.",
|
|
5
|
+
"owner": {
|
|
6
|
+
"name": "UltraContext",
|
|
7
|
+
"url": "https://ultracontext.ai"
|
|
8
|
+
},
|
|
9
|
+
"plugins": [
|
|
10
|
+
{
|
|
11
|
+
"name": "ultracontext",
|
|
12
|
+
"description": "Switch between AI agents with full context. /ultracontext:switch codex to continue your Claude session in Codex.",
|
|
13
|
+
"source": "./",
|
|
14
|
+
"category": "productivity"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: switch
|
|
3
|
+
description: "Switch current session to codex. UltraContext keeps your original context."
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Bash
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /switch — Cross-agent session portability by UltraContext
|
|
9
|
+
|
|
10
|
+
Switch your current conversation to another AI agent with full context.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
`/switch <target>` where target is: `codex` or `claude`
|
|
15
|
+
|
|
16
|
+
Optional flags: `--last N` (carry only last N messages), `--no-launch` (write session file only)
|
|
17
|
+
|
|
18
|
+
## Steps
|
|
19
|
+
|
|
20
|
+
1. Run:
|
|
21
|
+
```bash
|
|
22
|
+
ultracontext switch $ARGUMENTS
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
2. If `ultracontext` is not installed, tell the user: `npm i -g ultracontext` or `bun add -g ultracontext`
|
|
26
|
+
|
|
27
|
+
3. Report: session ID, file path, message count. Codex will open in a new terminal tab automatically.
|
package/postinstall.mjs
CHANGED
|
@@ -1,15 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// Post-install: register plugin skills with AI agents + launch onboarding
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
5
8
|
import process from "node:process";
|
|
9
|
+
import os from "node:os";
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
import { registerSkills } from "./lib/register-skills.mjs";
|
|
12
|
+
|
|
13
|
+
// skip when triggered by `ultracontext update`
|
|
8
14
|
if (process.env.ULTRACONTEXT_SKIP_POSTINSTALL) process.exit(0);
|
|
9
15
|
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const home = os.homedir();
|
|
18
|
+
|
|
10
19
|
const isGlobal = process.env.npm_config_global === "true";
|
|
11
20
|
const isTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
12
21
|
|
|
22
|
+
// read package version for managed-marker tracking
|
|
23
|
+
function readPackageVersion() {
|
|
24
|
+
try {
|
|
25
|
+
const pkg = JSON.parse(fs.readFileSync(join(__dirname, "package.json"), "utf8"));
|
|
26
|
+
return pkg.version ?? "0.0.0";
|
|
27
|
+
} catch { return "0.0.0"; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── register plugin skills with AI agents (global installs only) ───
|
|
31
|
+
// skipped for local/transitive installs so adding ultracontext as a dep can't
|
|
32
|
+
// silently mutate ~/.claude. Upgrades replace skills managed by ultracontext
|
|
33
|
+
// (tracked via sidecar .ultracontext-version file); user-customized SKILL.md
|
|
34
|
+
// (no sidecar) is preserved untouched.
|
|
35
|
+
if (isGlobal) {
|
|
36
|
+
registerSkills({
|
|
37
|
+
pluginDir: join(__dirname, "plugin", "skills"),
|
|
38
|
+
agentDirs: [join(home, ".claude", "skills"), join(home, ".codex", "skills")],
|
|
39
|
+
packageVersion: readPackageVersion(),
|
|
40
|
+
logger: (msg) => console.log(`ultracontext: ${msg}`),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── launch onboarding (global TTY installs only) ───────────────
|
|
45
|
+
|
|
13
46
|
if (isGlobal && isTTY) {
|
|
14
47
|
try {
|
|
15
48
|
execSync("ultracontext", { stdio: "inherit" });
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState } from "react";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import figlet from "figlet";
|
|
4
|
-
|
|
5
|
-
//#region ../sync/src/ui/hero-art.mjs
|
|
6
|
-
const HERO_TEXT = "UltraContext";
|
|
7
|
-
const HERO_FONT_ORDER = [
|
|
8
|
-
"Standard",
|
|
9
|
-
"Small",
|
|
10
|
-
"Mini",
|
|
11
|
-
"Slant"
|
|
12
|
-
];
|
|
13
|
-
const HERO_ART_CACHE = /* @__PURE__ */ new Map();
|
|
14
|
-
function trimBlankEdgeLines(lines) {
|
|
15
|
-
let start = 0;
|
|
16
|
-
let end = lines.length;
|
|
17
|
-
while (start < end && !String(lines[start] ?? "").trim()) start += 1;
|
|
18
|
-
while (end > start && !String(lines[end - 1] ?? "").trim()) end -= 1;
|
|
19
|
-
return lines.slice(start, end);
|
|
20
|
-
}
|
|
21
|
-
const HERO_FONT_ART = HERO_FONT_ORDER.map((font) => {
|
|
22
|
-
try {
|
|
23
|
-
const lines = trimBlankEdgeLines(figlet.textSync(HERO_TEXT, {
|
|
24
|
-
font,
|
|
25
|
-
horizontalLayout: "default",
|
|
26
|
-
verticalLayout: "default"
|
|
27
|
-
}).replace(/\n+$/g, "").split("\n").map((line) => line.replace(/\s+$/g, "")));
|
|
28
|
-
return {
|
|
29
|
-
lines,
|
|
30
|
-
width: Math.max(...lines.map((line) => line.length), 0)
|
|
31
|
-
};
|
|
32
|
-
} catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}).filter(Boolean);
|
|
36
|
-
function heroArtForWidth(columns) {
|
|
37
|
-
const available = Math.max(columns ?? 8, 8);
|
|
38
|
-
const cacheKey = String(available);
|
|
39
|
-
if (HERO_ART_CACHE.has(cacheKey)) return HERO_ART_CACHE.get(cacheKey);
|
|
40
|
-
const candidate = HERO_FONT_ART.find((entry) => entry.width <= available);
|
|
41
|
-
const art = candidate ? candidate.lines.map((line) => line.padEnd(candidate.width, " ")) : available >= 12 ? [HERO_TEXT] : ["UC"];
|
|
42
|
-
HERO_ART_CACHE.set(cacheKey, art);
|
|
43
|
-
return art;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
//#endregion
|
|
47
|
-
//#region ../sync/src/ui/constants.mjs
|
|
48
|
-
const UC_BRAND_BLUE = "#2f6fb3";
|
|
49
|
-
const UC_BLUE_LIGHT = "#7ec3ff";
|
|
50
|
-
const UC_CLAUDE_ORANGE = "#f4a261";
|
|
51
|
-
const UC_CODEX_BLUE = "#5fb2ff";
|
|
52
|
-
const UC_OPENCLAW_RED = "#e76f51";
|
|
53
|
-
const MENU_TABS = [
|
|
54
|
-
{
|
|
55
|
-
id: "logs",
|
|
56
|
-
label: "Live View"
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
id: "contexts",
|
|
60
|
-
label: "Contexts"
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
id: "configs",
|
|
64
|
-
label: "Configs"
|
|
65
|
-
}
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
//#endregion
|
|
69
|
-
//#region ../sync/src/Spinner.mjs
|
|
70
|
-
const WIDTH = 28;
|
|
71
|
-
const HEIGHT = 10;
|
|
72
|
-
const SCALE = 40;
|
|
73
|
-
const CAMERA_Z = 20;
|
|
74
|
-
const CHARS = "··..,,--::;;==!!**##$$@@";
|
|
75
|
-
const zBuffer = new Float32Array(WIDTH * HEIGHT);
|
|
76
|
-
const screenBuffer = new Uint8Array(WIDTH * HEIGHT);
|
|
77
|
-
const pointsData = [];
|
|
78
|
-
function addPoint(x, y, z, type) {
|
|
79
|
-
pointsData.push(x, y, z, type);
|
|
80
|
-
}
|
|
81
|
-
function addLine(x1, y1, z1, x2, y2, z2) {
|
|
82
|
-
const density = 15;
|
|
83
|
-
for (let i = 0; i <= density; i++) {
|
|
84
|
-
const t = i / density;
|
|
85
|
-
addPoint(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t, z1 + (z2 - z1) * t, 0);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
addLine(-1.8, -1.2, 0, -1.8, 1.2, 0);
|
|
89
|
-
addLine(-1.8, 1.2, 0, -1, 1.2, 0);
|
|
90
|
-
addLine(-1.8, -1.2, 0, -1, -1.2, 0);
|
|
91
|
-
addLine(1.8, -1.2, 0, 1.8, 1.2, 0);
|
|
92
|
-
addLine(1.8, 1.2, 0, 1, 1.2, 0);
|
|
93
|
-
addLine(1.8, -1.2, 0, 1, -1.2, 0);
|
|
94
|
-
addPoint(0, 0, 0, 1);
|
|
95
|
-
const points = new Float32Array(pointsData);
|
|
96
|
-
const pointCount = points.length / 4;
|
|
97
|
-
function renderFrame(angle) {
|
|
98
|
-
zBuffer.fill(-Infinity);
|
|
99
|
-
screenBuffer.fill(32);
|
|
100
|
-
const cosA = Math.cos(angle);
|
|
101
|
-
const sinA = Math.sin(angle);
|
|
102
|
-
for (let i = 0; i < pointCount; i++) {
|
|
103
|
-
const idx = i * 4;
|
|
104
|
-
const px = points[idx];
|
|
105
|
-
const py = points[idx + 1];
|
|
106
|
-
const pz = points[idx + 2];
|
|
107
|
-
const ptype = points[idx + 3];
|
|
108
|
-
const xRot = px * cosA - pz * sinA;
|
|
109
|
-
const yRot = py;
|
|
110
|
-
const zRot = px * sinA + pz * cosA;
|
|
111
|
-
const ooz = -1 / (zRot - CAMERA_Z);
|
|
112
|
-
const screenX = Math.floor(WIDTH / 2 + xRot * ooz * SCALE * 2);
|
|
113
|
-
const screenY = Math.floor(HEIGHT / 2 - yRot * ooz * SCALE);
|
|
114
|
-
if (screenX >= 0 && screenX < WIDTH && screenY >= 0 && screenY < HEIGHT) {
|
|
115
|
-
const bufIdx = screenX + screenY * WIDTH;
|
|
116
|
-
if (ooz > zBuffer[bufIdx]) {
|
|
117
|
-
zBuffer[bufIdx] = ooz;
|
|
118
|
-
if (ptype === 1) screenBuffer[bufIdx] = 79;
|
|
119
|
-
else {
|
|
120
|
-
let charIdx = Math.floor((zRot + 2) * 4.5);
|
|
121
|
-
if (charIdx < 0) charIdx = 0;
|
|
122
|
-
if (charIdx >= 24) charIdx = 23;
|
|
123
|
-
screenBuffer[bufIdx] = CHARS.charCodeAt(charIdx);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
const lines = [];
|
|
129
|
-
for (let y = 0; y < HEIGHT; y++) {
|
|
130
|
-
let row = "";
|
|
131
|
-
const offset = y * WIDTH;
|
|
132
|
-
for (let x = 0; x < WIDTH; x++) row += String.fromCharCode(screenBuffer[offset + x]);
|
|
133
|
-
lines.push(row);
|
|
134
|
-
}
|
|
135
|
-
return lines;
|
|
136
|
-
}
|
|
137
|
-
const Spinner = ({ color = "green", prefix = "", suffix = "", prefixColor = "white", suffixColor = "white", sideLines = [], sideGap = 0, sideColor = "white" }) => {
|
|
138
|
-
const [frameRows, setFrameRows] = useState(() => renderFrame(0));
|
|
139
|
-
useEffect(() => {
|
|
140
|
-
let angle = 0;
|
|
141
|
-
const timer = setInterval(() => {
|
|
142
|
-
angle += .05;
|
|
143
|
-
setFrameRows(renderFrame(angle));
|
|
144
|
-
}, 33);
|
|
145
|
-
timer.unref?.();
|
|
146
|
-
return () => clearInterval(timer);
|
|
147
|
-
}, []);
|
|
148
|
-
return React.createElement(Box, { flexDirection: "column" }, ...frameRows.map((row, index) => React.createElement(Text, { key: `spinner-row-${index}` }, prefix ? React.createElement(Text, { color: prefixColor }, prefix) : "", React.createElement(Text, { color }, row), sideLines.length > 0 ? React.createElement(Text, { color: sideColor }, `${" ".repeat(Math.max(sideGap, 0))}${sideLines[index] ?? ""}`) : "", suffix ? React.createElement(Text, { color: suffixColor }, suffix) : "")));
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
//#endregion
|
|
152
|
-
export { UC_CLAUDE_ORANGE as a, heroArtForWidth as c, UC_BRAND_BLUE as i, MENU_TABS as n, UC_CODEX_BLUE as o, UC_BLUE_LIGHT as r, UC_OPENCLAW_RED as s, Spinner as t };
|
|
153
|
-
//# sourceMappingURL=Spinner-CwBjkXHv.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Spinner-CwBjkXHv.mjs","names":[],"sources":["../../sync/src/ui/hero-art.mjs","../../sync/src/ui/constants.mjs","../../sync/src/Spinner.mjs"],"sourcesContent":["import figlet from \"figlet\";\n\nconst HERO_TEXT = \"UltraContext\";\nconst HERO_FONT_ORDER = [\"Standard\", \"Small\", \"Mini\", \"Slant\"];\nconst HERO_ART_CACHE = new Map();\n\nfunction trimBlankEdgeLines(lines) {\n let start = 0;\n let end = lines.length;\n while (start < end && !String(lines[start] ?? \"\").trim()) start += 1;\n while (end > start && !String(lines[end - 1] ?? \"\").trim()) end -= 1;\n return lines.slice(start, end);\n}\n\nconst HERO_FONT_ART = HERO_FONT_ORDER.map((font) => {\n try {\n const raw = figlet.textSync(HERO_TEXT, {\n font,\n horizontalLayout: \"default\",\n verticalLayout: \"default\",\n });\n const lines = trimBlankEdgeLines(\n raw\n .replace(/\\n+$/g, \"\")\n .split(\"\\n\")\n .map((line) => line.replace(/\\s+$/g, \"\"))\n );\n const width = Math.max(...lines.map((line) => line.length), 0);\n return { lines, width };\n } catch {\n return null;\n }\n}).filter(Boolean);\n\nexport function heroArtForWidth(columns) {\n const available = Math.max(columns ?? 8, 8);\n const cacheKey = String(available);\n if (HERO_ART_CACHE.has(cacheKey)) return HERO_ART_CACHE.get(cacheKey);\n\n const candidate = HERO_FONT_ART.find((entry) => entry.width <= available);\n const art = candidate\n ? candidate.lines.map((line) => line.padEnd(candidate.width, \" \"))\n : available >= 12\n ? [HERO_TEXT]\n : [\"UC\"];\n\n HERO_ART_CACHE.set(cacheKey, art);\n return art;\n}\n","export const UC_BRAND_BLUE = \"#2f6fb3\";\nexport const UC_BLUE_LIGHT = \"#7ec3ff\";\nexport const UC_CLAUDE_ORANGE = \"#f4a261\";\nexport const UC_CODEX_BLUE = \"#5fb2ff\";\nexport const UC_OPENCLAW_RED = \"#e76f51\";\n\nexport const MENU_TABS = [\n { id: \"logs\", label: \"Live View\" },\n { id: \"contexts\", label: \"Contexts\" },\n { id: \"configs\", label: \"Configs\" },\n];\n","import React, { useState, useEffect } from \"react\";\nimport { Box, Text } from \"ink\";\n\nconst WIDTH = 28;\nconst HEIGHT = 10;\nconst SCALE = 40.0;\nconst CAMERA_Z = 20.0;\nconst CHARS = \"··..,,--::;;==!!**##$$@@\";\n\nconst zBuffer = new Float32Array(WIDTH * HEIGHT);\nconst screenBuffer = new Uint8Array(WIDTH * HEIGHT);\n\nconst pointsData = [];\nfunction addPoint(x, y, z, type) {\n pointsData.push(x, y, z, type);\n}\n\nfunction addLine(x1, y1, z1, x2, y2, z2) {\n const density = 15;\n for (let i = 0; i <= density; i++) {\n const t = i / density;\n addPoint(\n x1 + (x2 - x1) * t,\n y1 + (y2 - y1) * t,\n z1 + (z2 - z1) * t,\n 0\n );\n }\n}\n\naddLine(-1.8, -1.2, 0, -1.8, 1.2, 0);\naddLine(-1.8, 1.2, 0, -1.0, 1.2, 0);\naddLine(-1.8, -1.2, 0, -1.0, -1.2, 0);\n\naddLine(1.8, -1.2, 0, 1.8, 1.2, 0);\naddLine(1.8, 1.2, 0, 1.0, 1.2, 0);\naddLine(1.8, -1.2, 0, 1.0, -1.2, 0);\n\naddPoint(0, 0, 0, 1);\n\nconst points = new Float32Array(pointsData);\nconst pointCount = points.length / 4;\n\nfunction renderFrame(angle) {\n zBuffer.fill(-Infinity);\n screenBuffer.fill(32);\n\n const cosA = Math.cos(angle);\n const sinA = Math.sin(angle);\n\n for (let i = 0; i < pointCount; i++) {\n const idx = i * 4;\n const px = points[idx];\n const py = points[idx + 1];\n const pz = points[idx + 2];\n const ptype = points[idx + 3];\n\n const xRot = px * cosA - pz * sinA;\n const yRot = py;\n const zRot = px * sinA + pz * cosA;\n\n const zFinal = zRot - CAMERA_Z;\n const ooz = -1.0 / zFinal;\n\n const screenX = Math.floor(WIDTH / 2 + xRot * ooz * SCALE * 2.0);\n const screenY = Math.floor(HEIGHT / 2 - yRot * ooz * SCALE);\n\n if (screenX >= 0 && screenX < WIDTH && screenY >= 0 && screenY < HEIGHT) {\n const bufIdx = screenX + screenY * WIDTH;\n if (ooz > zBuffer[bufIdx]) {\n zBuffer[bufIdx] = ooz;\n if (ptype === 1) {\n screenBuffer[bufIdx] = 79;\n } else {\n let charIdx = Math.floor((zRot + 2.0) * 4.5);\n if (charIdx < 0) charIdx = 0;\n if (charIdx >= CHARS.length) charIdx = CHARS.length - 1;\n screenBuffer[bufIdx] = CHARS.charCodeAt(charIdx);\n }\n }\n }\n }\n\n const lines = [];\n for (let y = 0; y < HEIGHT; y++) {\n let row = \"\";\n const offset = y * WIDTH;\n for (let x = 0; x < WIDTH; x++) {\n row += String.fromCharCode(screenBuffer[offset + x]);\n }\n lines.push(row);\n }\n return lines;\n}\n\nconst Spinner = ({\n color = \"green\",\n prefix = \"\",\n suffix = \"\",\n prefixColor = \"white\",\n suffixColor = \"white\",\n sideLines = [],\n sideGap = 0,\n sideColor = \"white\",\n}) => {\n const [frameRows, setFrameRows] = useState(() => renderFrame(0));\n\n useEffect(() => {\n let angle = 0;\n const timer = setInterval(() => {\n angle += 0.05;\n setFrameRows(renderFrame(angle));\n }, 33);\n timer.unref?.();\n return () => clearInterval(timer);\n }, []);\n\n return React.createElement(\n Box,\n { flexDirection: \"column\" },\n ...frameRows.map((row, index) =>\n React.createElement(\n Text,\n { key: `spinner-row-${index}` },\n prefix ? React.createElement(Text, { color: prefixColor }, prefix) : \"\",\n React.createElement(Text, { color }, row),\n sideLines.length > 0\n ? React.createElement(Text, { color: sideColor }, `${\" \".repeat(Math.max(sideGap, 0))}${sideLines[index] ?? \"\"}`)\n : \"\",\n suffix ? React.createElement(Text, { color: suffixColor }, suffix) : \"\"\n )\n )\n );\n};\n\nexport default Spinner;\n"],"mappings":"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"}
|