gft-merge 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CoralLips
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,86 @@
1
- # Temporary Holding Version
1
+ <p align="center"><strong>English</strong> · <a href="README_ZH.md">简体中文</a></p>
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # GFT-Merge
4
+
5
+ ## Carry context between your AI coding sessions.
6
+
7
+ **Pull sessions from Claude Code and Codex into a pool, then start a new session in either tool with that context already in it.**
8
+
9
+ You plan something in one Claude Code session, build it in Codex, and debug it in another session. Handing over means copying and pasting by hand. GFT-Merge reads your local sessions, lets you combine the ones that matter, and starts a fresh session whose first message carries them, so the AI picks up where you left off.
10
+
11
+ It runs on your computer and opens in your browser. A sibling of [GFT-Map](https://github.com/CoralLips/gft-map).
12
+
13
+ > The interface is in Chinese for now.
14
+
15
+ ## Get started
16
+
17
+ You'll need **Node.js 22+** and at least one of:
18
+
19
+ - **Claude Code** in the **Claude desktop app**, signed in. New Claude sessions are brought into the app through its own `claude://` link, so the app is required to see them.
20
+ - **Codex**, the Codex app or CLI, signed in. With the Codex app, new sessions also open there.
21
+
22
+ Then run:
23
+
24
+ ```sh
25
+ npx gft-merge
26
+ ```
27
+
28
+ Your browser opens `http://127.0.0.1:4319`. Run it again any time; it reuses the running service.
29
+
30
+ ## How it works
31
+
32
+ The page has two halves. At the top, one column per tool lists your sessions from the last 30 days. Below, your pools on the left and the selected pool's workspace on the right. Everything is done by dragging.
33
+
34
+ | What you want | How |
35
+ | --------------------------------- | ----------------------------------------------------------------------------------------------- |
36
+ | Put a session in a pool | Drag it from a tool's column into the workspace. It becomes a ball. |
37
+ | Combine sessions | Drop one ball on another. Any two balls can be fused. |
38
+ | Start a session with that context | Drag a ball up onto a tool's column. A new session starts there, named "merge · pool name". |
39
+ | Make a big ball smaller | Click the ball to compress it with a model (see below). |
40
+ | See what a ball holds | Hover the name under it. Drag a session out of that list to split it off. |
41
+ | Move or remove | Drop a ball on another pool in the list to move it; drop it outside the workspace to remove it. |
42
+
43
+ **The number in a ball** is its size in tokens. A single session shows that session's own context size, read from the tool's records. A fused or compressed ball shows what dragging it up would send, counted locally (marked ≈).
44
+
45
+ **Compression** sends the ball's content to the tool and model chosen under **Settings → 处理工具** (processing tool) and replaces it with the result. While it runs, the ball's ring turns and its name reads "压缩中…" (compressing). A compressed ball can't be split again; you can fuse more sessions onto it or compress it again. Drag it up before compression finishes and the original text is sent.
46
+
47
+ **The new session's first message** contains the plain conversation of every session in the ball (your messages and the final answer of each turn, without tool output) and asks the AI to report briefly what it understood (what the sessions are about, where things stand, what is decided and open, what comes next), then wait for you. That reply is a real turn, so the session sits at the top of the tool's list. With Codex the page returns at once and the session opens when the reply ends; with Claude the page waits for the reply (several seconds to about a minute), then the session opens in the Claude app.
48
+
49
+ **Why only new sessions:** Codex refuses other programs writing to a session that is open in the Codex app, and the Claude app shows its own copy of a session, not what another program writes into it. Neither is reliable, so GFT-Merge always starts a new session.
50
+
51
+ ## Where does your data go?
52
+
53
+ - GFT-Merge reads your local Claude Code and Codex session files. It does not change them, and it does not install hooks or change either tool's settings.
54
+ - Pools are stored in `~/.gft-merge/` on your computer (set `GFT_MERGE_HOME` to move them).
55
+ - A model is called only in two cases, both started by you: dragging a ball up (the new session runs in that tool, on your account) and compressing (one request to the processing tool, on your account).
56
+ - No GFT account, no cloud.
57
+
58
+ ## Commands
59
+
60
+ ```sh
61
+ npx gft-merge # open the page, starting the service if needed
62
+ npx gft-merge stop # stop the service
63
+ npx gft-merge status # show whether it runs, and where the data is
64
+ ```
65
+
66
+ Add `--port <number>` to use another port.
67
+
68
+ ## Project status and feedback
69
+
70
+ Version 0.1, an early release. Tested on Windows; on macOS and Linux, opening sessions in the desktop apps is not verified yet. Supports Claude Code and Codex.
71
+
72
+ Share problems or ideas in [Issues](https://github.com/CoralLips/gft-merge/issues).
73
+
74
+ ## Development
75
+
76
+ ```sh
77
+ git clone https://github.com/CoralLips/gft-merge.git
78
+ cd gft-merge
79
+ npm install
80
+ npm test
81
+ node cli.mjs open
82
+ ```
83
+
84
+ ## License
85
+
86
+ MIT
package/README_ZH.md ADDED
@@ -0,0 +1,84 @@
1
+ <p align="center"><a href="README.md">English</a> · <strong>简体中文</strong></p>
2
+
3
+ # GFT-Merge
4
+
5
+ ## 让上下文在 AI 编程会话之间流动。
6
+
7
+ **把 Claude Code 和 Codex 的会话拉进一个池子,再在任一工具里开一个新会话,上下文已经带在里面。**
8
+
9
+ 你在一个 Claude Code 会话里定方案,在 Codex 里写代码,又在另一个会话里排查问题。每次交接都得手动复制粘贴。GFT-Merge 读取你本机的会话,让你把有用的几段合在一起,然后开一个新会话、把它们作为第一条消息带进去,AI 就能接着往下干。
10
+
11
+ 在你自己的电脑上运行,在浏览器里打开。和 [GFT-Map](https://github.com/CoralLips/gft-map) 是同一系列的工具。
12
+
13
+ ## 开始
14
+
15
+ 需要 **Node.js 22 以上**,以及下面至少一个:
16
+
17
+ - **Claude 桌面应用里的 Claude Code**,已登录。新建的 Claude 会话要通过桌面应用自己的 `claude://` 链接导入,所以必须有桌面应用才能看到。
18
+ - **Codex**,桌面应用或命令行都可以,已登录。装了 Codex 桌面应用的话,新会话也会在里面打开。
19
+
20
+ 然后运行:
21
+
22
+ ```sh
23
+ npx gft-merge
24
+ ```
25
+
26
+ 浏览器会打开 `http://127.0.0.1:4319`。之后随时再运行一次即可,已经在跑的服务会直接复用。
27
+
28
+ ## 怎么用
29
+
30
+ 页面分上下两半。上面每个工具一栏,列出最近 30 天的会话;下面左边是池子列表,右边是选中池子的工作区。所有操作都靠拖。
31
+
32
+ | 想做什么 | 怎么做 |
33
+ | ---------------------- | -------------------------------------------------------------------- |
34
+ | 把会话放进池子 | 从上面的工具栏拖进工作区,它就成了一个球 |
35
+ | 合并几个会话 | 把一个球拖到另一个球上,任意两个球都能融合 |
36
+ | 带着这些上下文开新会话 | 把球往上拖到某个工具的栏上,就在那里新建会话,名为「merge · 池子名」 |
37
+ | 把大球变小 | 点一下球,用模型压缩(见下文) |
38
+ | 看球里有什么 | 鼠标悬停在球下方的名字上;从列表里把某个会话拖出去就能拆开 |
39
+ | 搬走或移出 | 把球拖到左边另一个池子上就搬过去;拖出工作区就移出池子 |
40
+
41
+ **球里的数字**是 token 数。单个会话的球显示这个会话本身的上下文大小,直接读自工具的记录;融合或压缩过的球显示往上拖时要送出去的量,由本地分词器数出来,前面带 ≈。
42
+
43
+ **压缩**会把球的内容发给 **设置 → 处理工具** 里选的工具和模型,用结果替换原内容。压缩中球的虚线圈会转,名字处显示“压缩中…”。压完的球不能再拆,但可以继续往上融合新的会话,也可以再压。压缩没完成时往上拖,送出去的是原文。
44
+
45
+ **新会话的第一条消息**包含球里每个会话的原话(你说的话和 AI 每轮的最终回答,不含工具输出),并请 AI 先简要汇报它的理解(这些会话在做什么、进展到哪、定了什么、没定什么、下一步做什么),然后等你的指示。这一轮是真对话,所以新会话排在工具列表最上面。Codex 那边页面立刻返回,回复结束后在 Codex 里打开;Claude 那边要等回复结束(几秒到一分钟左右),然后在 Claude 桌面应用里打开。
46
+
47
+ **为什么只做新建:** Codex 会话在 Codex 桌面应用里开着时,Codex 不让别的程序往里写;Claude 桌面应用显示的是它自己保存的会话内容,别的程序写进去的消息它不显示。两条路都靠不住,所以 GFT-Merge 只新建会话。
48
+
49
+ ## 数据去哪了
50
+
51
+ - GFT-Merge 读取你本机 Claude Code 和 Codex 的会话文件,不修改它们,也不装钩子、不改两家工具的任何设置。
52
+ - 池子存在你电脑上的 `~/.gft-merge/`(可用 `GFT_MERGE_HOME` 改位置)。
53
+ - 只有两种情况会调用模型,都由你触发:往上拖新建会话(在那个工具里、用你的账号额度),以及压缩(向处理工具发一次请求,用你的账号额度)。
54
+ - 不需要 GFT 账号,不上云。
55
+
56
+ ## 命令
57
+
58
+ ```sh
59
+ npx gft-merge # 打开页面,需要时自动启动服务
60
+ npx gft-merge stop # 停止服务
61
+ npx gft-merge status # 查看是否在运行、数据在哪
62
+ ```
63
+
64
+ 加 `--port <端口>` 可以换端口。
65
+
66
+ ## 项目状态和反馈
67
+
68
+ 0.1 版,早期版本。在 Windows 上实测过;macOS 和 Linux 上在桌面应用里打开会话还没验证。支持 Claude Code 和 Codex。
69
+
70
+ 问题和想法欢迎提在 [Issues](https://github.com/CoralLips/gft-merge/issues)。
71
+
72
+ ## 开发
73
+
74
+ ```sh
75
+ git clone https://github.com/CoralLips/gft-merge.git
76
+ cd gft-merge
77
+ npm install
78
+ npm test
79
+ node cli.mjs open
80
+ ```
81
+
82
+ ## 许可
83
+
84
+ MIT
package/cli.mjs ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ // gft-merge: open | serve | stop | status
3
+ import { spawn } from 'node:child_process';
4
+ import { connect } from 'node:net';
5
+ import { mkdir, open as openFile, rm } from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { homeDir, readJson, writeJson } from './home.mjs';
9
+ import { DEFAULT_PORT, startServer } from './server.mjs';
10
+
11
+ // The command may be left out, as in `gft-merge --port 4329`: then it is `open`.
12
+ const args = process.argv.slice(2);
13
+ const command = args[0] && !args[0].startsWith('--') ? args[0] : 'open';
14
+ const portAt = args.indexOf('--port');
15
+ const port =
16
+ (portAt >= 0 && Number(args[portAt + 1])) || Number(process.env.GFT_MERGE_PORT) || DEFAULT_PORT;
17
+ const url = `http://127.0.0.1:${port}`;
18
+ const serviceFile = path.join(homeDir(), 'service.json');
19
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
20
+
21
+ const portTaken = () =>
22
+ new Error(
23
+ `端口 ${port} 已被其他程序占用,GFT-Merge 没有启动。请换一个端口,例如:npx gft-merge --port ${port + 10}`,
24
+ );
25
+
26
+ /** Whether anything listens on the port, without speaking HTTP to it. */
27
+ const listening = () =>
28
+ new Promise(resolve => {
29
+ const socket = connect({ host: '127.0.0.1', port }, () => {
30
+ socket.destroy();
31
+ resolve(true);
32
+ });
33
+ socket.once('error', () => resolve(false));
34
+ socket.setTimeout(1500, () => {
35
+ socket.destroy();
36
+ resolve(true);
37
+ });
38
+ });
39
+
40
+ /** 'ours' when GFT-Merge answers on the port, 'other' when something else holds it, else 'free'. */
41
+ async function probe() {
42
+ try {
43
+ const response = await fetch(`${url}/api/state`, { signal: AbortSignal.timeout(1500) });
44
+ if (response.ok && (await response.json().catch(() => null))?.product === 'gft-merge')
45
+ return 'ours';
46
+ } catch {
47
+ // Not an HTTP answer from us; see whether the port is taken at all.
48
+ }
49
+ return (await listening()) ? 'other' : 'free';
50
+ }
51
+ const running = async () => (await probe()) === 'ours';
52
+
53
+ function openBrowser() {
54
+ const [file, args] =
55
+ process.platform === 'win32'
56
+ ? ['cmd', ['/c', 'start', '', url]]
57
+ : [process.platform === 'darwin' ? 'open' : 'xdg-open', [url]];
58
+ spawn(file, args, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
59
+ }
60
+
61
+ const commands = {
62
+ async serve() {
63
+ await startServer({ port }).catch(error => {
64
+ throw error.code === 'EADDRINUSE' ? portTaken() : error;
65
+ });
66
+ await writeJson(serviceFile, { pid: process.pid, port, startedAt: new Date().toISOString() });
67
+ console.log(`GFT-Merge: ${url}`);
68
+ },
69
+
70
+ async open() {
71
+ const state = await probe();
72
+ if (state === 'other') throw portTaken();
73
+ if (state === 'free') {
74
+ await mkdir(homeDir(), { recursive: true });
75
+ const log = await openFile(path.join(homeDir(), 'service.log'), 'a');
76
+ const script = fileURLToPath(import.meta.url);
77
+ spawn(process.execPath, [script, 'serve', '--port', String(port)], {
78
+ detached: true,
79
+ windowsHide: true,
80
+ stdio: ['ignore', log.fd, log.fd],
81
+ }).unref();
82
+ await log.close();
83
+ for (let i = 0; i < 50 && !(await running()); i++) await delay(100);
84
+ if (!(await running()))
85
+ throw new Error(`启动失败,请查看 ${path.join(homeDir(), 'service.log')}`);
86
+ }
87
+ openBrowser();
88
+ console.log(`GFT-Merge 已打开:${url}`);
89
+ },
90
+
91
+ /** Stops the process this data directory started, as recorded when it began serving. */
92
+ async stop() {
93
+ const service = await readJson(serviceFile);
94
+ const alive = pid => {
95
+ try {
96
+ process.kill(pid, 0);
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ };
102
+ if (!service?.pid || !alive(service.pid)) {
103
+ await rm(serviceFile, { force: true });
104
+ return console.log('GFT-Merge 没有在运行。');
105
+ }
106
+ process.kill(service.pid);
107
+ await rm(serviceFile, { force: true });
108
+ console.log('GFT-Merge 已停止。');
109
+ },
110
+
111
+ async status() {
112
+ console.log(
113
+ JSON.stringify(
114
+ {
115
+ running: await running(),
116
+ url,
117
+ data: homeDir(),
118
+ },
119
+ null,
120
+ 2,
121
+ ),
122
+ );
123
+ },
124
+ };
125
+
126
+ if (!commands[command]) {
127
+ console.error(`用法:gft-merge ${Object.keys(commands).join(' | ')} [--port 端口]`);
128
+ process.exit(1);
129
+ }
130
+ try {
131
+ await commands[command]();
132
+ } catch (error) {
133
+ console.error(error.message);
134
+ process.exit(1);
135
+ }
package/home.mjs ADDED
@@ -0,0 +1,63 @@
1
+ // Where GFT-Merge keeps its files, and small helpers to read and write them safely.
2
+ import { mkdir, readFile, rename, writeFile, appendFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ export const homeDir = () =>
7
+ path.resolve(process.env.GFT_MERGE_HOME || path.join(os.homedir(), '.gft-merge'));
8
+
9
+ /** Text placed at the start of everything GFT-Merge writes into a session. */
10
+ export const MARKER = '〔GFT-Merge〕';
11
+ /**
12
+ * Closes the first message of a session a ball starts: the AI reports what it
13
+ * understood, so the person can check it before going on, and waits for them.
14
+ */
15
+ export const BRIEF =
16
+ '\n\n读完以上内容后,先简要汇报你的理解(几句话即可):这些会话在做什么、进展到哪一步、已经定下的结论、还没定的问题,以及你认为接下来该做什么。先不要执行任何操作,等我的指示。';
17
+ /** What a processing tool is asked when a ball is compressed; the ball's content follows. */
18
+ export const COMPRESS =
19
+ '把下面几段会话记录压缩成一份,供另一个 AI 接着工作:保留结论、决定、没定的问题和关键事实,去掉重复和过程,保持原文语言,标明各条来自哪个会话。只输出压缩结果。\n\n';
20
+
21
+ export async function readJson(file, fallback = null) {
22
+ try {
23
+ return JSON.parse(await readFile(file, 'utf8'));
24
+ } catch (error) {
25
+ if (error.code === 'ENOENT') return fallback;
26
+ throw error;
27
+ }
28
+ }
29
+
30
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
31
+
32
+ /** Writes through a temporary file. Windows briefly refuses renames while a reader holds the file. */
33
+ export async function writeJson(file, value) {
34
+ await mkdir(path.dirname(file), { recursive: true });
35
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
36
+ await writeFile(temporary, JSON.stringify(value, null, 2));
37
+ for (let attempt = 0; ; attempt++) {
38
+ try {
39
+ return await rename(temporary, file);
40
+ } catch (error) {
41
+ if (!['EPERM', 'EACCES', 'EBUSY'].includes(error.code) || attempt >= 20) throw error;
42
+ await delay(Math.min(25 * 2 ** attempt, 250));
43
+ }
44
+ }
45
+ }
46
+
47
+ export async function appendLines(file, values) {
48
+ if (!values.length) return;
49
+ await mkdir(path.dirname(file), { recursive: true });
50
+ await appendFile(file, values.map(value => JSON.stringify(value)).join('\n') + '\n');
51
+ }
52
+
53
+ export async function readLines(file) {
54
+ try {
55
+ return (await readFile(file, 'utf8'))
56
+ .split('\n')
57
+ .filter(Boolean)
58
+ .map(line => JSON.parse(line));
59
+ } catch (error) {
60
+ if (error.code === 'ENOENT') return [];
61
+ throw error;
62
+ }
63
+ }
package/openLink.mjs ADDED
@@ -0,0 +1,11 @@
1
+ // Opens a link with the system's handler, the way a click would, e.g. claude://resume
2
+ // so the Claude app shows a session another program wrote.
3
+ import { spawn } from 'node:child_process';
4
+
5
+ export function openLink(url) {
6
+ const [file, args] =
7
+ process.platform === 'win32'
8
+ ? ['cmd', ['/c', 'start', '', url]]
9
+ : [process.platform === 'darwin' ? 'open' : 'xdg-open', [url]];
10
+ spawn(file, args, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
11
+ }
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "gft-merge",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Merge AI agent sessions into pools and start a new session from a pool in any tool.",
5
+ "keywords": [
6
+ "ai",
7
+ "agent",
8
+ "claude-code",
9
+ "codex",
10
+ "session",
11
+ "context",
12
+ "merge"
13
+ ],
14
+ "homepage": "https://github.com/CoralLips/gft-merge#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/CoralLips/gft-merge/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/CoralLips/gft-merge.git"
21
+ },
22
+ "license": "MIT",
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=22"
26
+ },
27
+ "bin": {
28
+ "gft-merge": "cli.mjs"
29
+ },
30
+ "files": [
31
+ "cli.mjs",
32
+ "home.mjs",
33
+ "openLink.mjs",
34
+ "pools.mjs",
35
+ "server.mjs",
36
+ "tools/",
37
+ "web/",
38
+ "skill/",
39
+ "README_ZH.md"
40
+ ],
41
+ "scripts": {
42
+ "start": "node cli.mjs open",
43
+ "test": "node --test \"tests/*.test.mjs\""
44
+ },
45
+ "dependencies": {
46
+ "@anthropic-ai/claude-agent-sdk": "0.3.270",
47
+ "gpt-tokenizer": "4.0.0"
48
+ }
49
+ }