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.
Files changed (41) hide show
  1. package/dist/cli/entry.mjs +9 -3
  2. package/dist/cli/entry.mjs.map +1 -1
  3. package/dist/cli/onboarding.mjs +268 -111
  4. package/dist/cli/onboarding.mjs.map +1 -1
  5. package/dist/cli/sdk-sync.mjs +199 -939
  6. package/dist/cli/sdk-sync.mjs.map +1 -1
  7. package/dist/cli/switch.mjs +168 -0
  8. package/dist/cli/switch.mjs.map +1 -0
  9. package/dist/{ctl-CXfNEPN8.mjs → ctl-DTQZxn3N.mjs} +2 -2
  10. package/dist/{ctl-CXfNEPN8.mjs.map → ctl-DTQZxn3N.mjs.map} +1 -1
  11. package/dist/hero-art-C03HmDXN.mjs +46 -0
  12. package/dist/hero-art-C03HmDXN.mjs.map +1 -0
  13. package/dist/index.d.mts +21 -1
  14. package/dist/index.d.mts.map +1 -1
  15. package/dist/index.mjs +25 -3
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/{launcher-BMMjzr5k.mjs → launcher-ZylswrpR.mjs} +3 -3
  18. package/dist/{launcher-BMMjzr5k.mjs.map → launcher-ZylswrpR.mjs.map} +1 -1
  19. package/dist/{lock-5aJnda81.mjs → lock-BhZX2aF3.mjs} +2 -2
  20. package/dist/{lock-5aJnda81.mjs.map → lock-BhZX2aF3.mjs.map} +1 -1
  21. package/dist/onboarding-preferences-Alhblobi.mjs +76 -0
  22. package/dist/onboarding-preferences-Alhblobi.mjs.map +1 -0
  23. package/dist/src-Bovo1ukU.mjs +1200 -0
  24. package/dist/src-Bovo1ukU.mjs.map +1 -0
  25. package/dist/{tui-DZ1SDOH2.mjs → tui-DLEjew3K.mjs} +334 -115
  26. package/dist/tui-DLEjew3K.mjs.map +1 -0
  27. package/dist/utils-BTfShW0g.mjs +36 -0
  28. package/dist/utils-BTfShW0g.mjs.map +1 -0
  29. package/dist/{utils-CmuIYHtm.mjs → utils-D9CKnbke.mjs} +26 -34
  30. package/dist/utils-D9CKnbke.mjs.map +1 -0
  31. package/lib/register-skills.mjs +96 -0
  32. package/package.json +8 -3
  33. package/plugin/.claude-plugin/plugin.json +6 -0
  34. package/plugin/README.md +112 -0
  35. package/plugin/marketplace.json +17 -0
  36. package/plugin/skills/switch/SKILL.md +27 -0
  37. package/postinstall.mjs +35 -2
  38. package/dist/Spinner-CwBjkXHv.mjs +0 -153
  39. package/dist/Spinner-CwBjkXHv.mjs.map +0 -1
  40. package/dist/tui-DZ1SDOH2.mjs.map +0 -1
  41. 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
- //#endregion
73
- //#region ../sync/src/utils.mjs
74
- function sha256(value) {
75
- return crypto.createHash("sha256").update(value).digest("hex");
76
- }
77
- function toInt(value, fallback) {
78
- const parsed = Number.parseInt(String(value ?? ""), 10);
79
- return Number.isFinite(parsed) ? parsed : fallback;
80
- }
81
- function boolFromEnv(value, fallback = false) {
82
- if (value === void 0) return fallback;
83
- const normalized = String(value).trim().toLowerCase();
84
- if ([
85
- "1",
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 { asIso as a, extractSessionIdFromPath as c, normalizeWhitespace as d, preserveText as f, truncateString as h, toInt as i, firstMessageTimestamp as l, toMessage as m, extractProjectPathFromFile as n, coerceMessageText as o, safeJsonParse as p, sha256 as r, expandHome as s, boolFromEnv as t, normalizeRole as u };
106
- //# sourceMappingURL=utils-CmuIYHtm.mjs.map
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.4.13",
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
  },
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "ultracontext",
3
+ "description": "Cross-agent session portability. Powered by UltraContext.",
4
+ "author": { "name": "UltraContext", "url": "https://ultracontext.ai" },
5
+ "skills": "./skills/"
6
+ }
@@ -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
- // Auto-launch onboarding after global install (skip in CI / non-TTY / local installs)
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
- // skip when triggered by `ultracontext update` to prevent duplicate launch
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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"}