gft-merge 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +70 -2
- package/README_ZH.md +71 -0
- package/cli.mjs +132 -0
- package/home.mjs +63 -0
- package/openLink.mjs +11 -0
- package/package.json +47 -4
- package/pools.mjs +409 -0
- package/server.mjs +330 -0
- package/skill/SKILL.md +28 -0
- package/tools/claude.mjs +384 -0
- package/tools/codex.mjs +386 -0
- package/web/app.js +946 -0
- package/web/index.html +72 -0
- package/web/style.css +638 -0
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,71 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center"><strong>English</strong> · <a href="README_ZH.md">简体中文</a></p>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# GFT-Merge
|
|
4
|
+
|
|
5
|
+
Pull sessions from different AI coding tools into a **pool**, then start a new session in any of them from that pool. Runs locally; your data stays on your machine.
|
|
6
|
+
|
|
7
|
+
Works with **Claude Code** and **Codex** today.
|
|
8
|
+
|
|
9
|
+
## Get started
|
|
10
|
+
|
|
11
|
+
You need Node.js 22 or newer, and Claude Code and/or Codex installed and signed in.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx gft-merge
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Your browser opens `http://127.0.0.1:4319`. You can also install it globally and run `gft-merge`.
|
|
18
|
+
|
|
19
|
+
## How it works
|
|
20
|
+
|
|
21
|
+
- **Top**: one bucket per tool, listing the last 30 days of sessions. The Claude list matches the Claude app: archived and deleted sessions are left out and names follow the app; sessions started in a terminal are marked "CLI".
|
|
22
|
+
- **Bottom**: your pools on the left, the selected pool's workspace on the right.
|
|
23
|
+
- **Drag down to add**: a session dropped into the workspace becomes a ball; dropped on a ball, it fuses into it.
|
|
24
|
+
- **Drop a ball on a ball to fuse**: any two balls can be fused.
|
|
25
|
+
- **Drag up to start a session**: drop a ball on a tool's bucket to start a new session there whose first message is the ball's content.
|
|
26
|
+
- **Click a ball to compress**: one model call, using the tool and model chosen as the processor in settings. While it runs the ball's dashed ring turns and its name reads "压缩中…". The result is one piece that 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.
|
|
27
|
+
- **The number in a ball**: a single session shows that session's own context size, read from the tool's records; a fused or compressed ball shows how many tokens dragging it up would send, counted locally (≈).
|
|
28
|
+
- **Names and contents**: the label under a ball lists its sessions, each prefixed with its tool. Hover the label to see the contents; drag a session out of that list to split it off.
|
|
29
|
+
- **Move and remove**: drop a ball on another pool in the list to move it; drop it outside the workspace to remove it.
|
|
30
|
+
|
|
31
|
+
## What happens when you drag up
|
|
32
|
+
|
|
33
|
+
GFT-Merge starts a session in that tool (named "merge · pool name"), sends the content as its first message, 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) and then wait for you. That is a real turn, so the new session sits at the top of the tool's list. No hooks are installed, no tool settings are changed, and nothing runs inside the tools' own processing.
|
|
34
|
+
|
|
35
|
+
- **Codex**: returns as soon as the message is sent; once the reply ends, the session opens in the Codex app through its own `codex://` link.
|
|
36
|
+
- **Claude Code**: waits for Claude's reply (from several seconds to a minute or so, depending on length), then imports and opens the session in the Claude app through its own `claude://resume` link.
|
|
37
|
+
- The new session opens in the project most of its sources came from.
|
|
38
|
+
|
|
39
|
+
Why not send into an existing session: Codex refuses other programs writing to a session that is open in the Codex app, and the Claude app shows its own cached copy, not what another program writes into a session file. Neither path is reliable, so only new sessions are supported.
|
|
40
|
+
|
|
41
|
+
Notes:
|
|
42
|
+
|
|
43
|
+
- What is sent is the plain conversation: your messages and the final answer of each turn, without intermediate steps or tool logs. A model is called only when you compress, and that uses your quota.
|
|
44
|
+
- Uncompressed content is sent in full; a long pool can fill the new session's context, so compress it first.
|
|
45
|
+
- Tested on Windows so far; opening the desktop apps' links on macOS and Linux is not verified.
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
`open` (default) · `serve` · `stop` · `status`, with an optional `--port`.
|
|
50
|
+
|
|
51
|
+
## Data
|
|
52
|
+
|
|
53
|
+
Stored in `~/.gft-merge/` (set `GFT_MERGE_HOME` to change):
|
|
54
|
+
|
|
55
|
+
- `pools/<id>.json`: the pool's name, its balls, and each ball's sessions and compacts.
|
|
56
|
+
- `pools/<id>.jsonl`: the pool's messages, one per line. When sessions are compressed their messages are removed and only the compact remains; the original sessions in each tool are untouched.
|
|
57
|
+
- `settings.json`: which tools are enabled, and the processor's tool and model.
|
|
58
|
+
|
|
59
|
+
Everything GFT-Merge sends starts with `〔GFT-Merge〕`; merging skips such messages, so content does not snowball back and forth.
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install
|
|
65
|
+
npm test
|
|
66
|
+
node cli.mjs open
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
package/README_ZH.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
## 开始
|
|
10
|
+
|
|
11
|
+
需要 Node.js 22 以上,以及已经装好并登录的 Claude Code 和/或 Codex。
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx gft-merge
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
浏览器会打开 `http://127.0.0.1:4319`。也可以全局安装后运行 `gft-merge`。
|
|
18
|
+
|
|
19
|
+
## 用法
|
|
20
|
+
|
|
21
|
+
- **上面**:每个工具一栏,列出最近 30 天的会话。Claude 的列表跟桌面端一致:已归档、已删除的不列,名字用桌面端的;终端里开的会话标「CLI」。
|
|
22
|
+
- **下面**:左边是池子列表,右边是选中池子的工作区。
|
|
23
|
+
- **往下拖=放进池子**:会话拖进工作区就成一个球;拖到已有的球上就融合进去。
|
|
24
|
+
- **球拖到球上=融合**:任意两个球都能融合。
|
|
25
|
+
- **往上拖=新建**:把一个球拖到某个工具的区域上,就在那个工具里新建一个会话,第一条消息就是这个球的内容。
|
|
26
|
+
- **点一下球=压缩**:用设置里选的“处理工具”(哪个工具的哪个模型)调一次模型,把球的内容压成一份。压缩中球的虚线圈会转,名字处显示“压缩中…”。压完成为一个整体,不能再拆;可以继续往上融合新的会话,也可以再压。压缩没完成时拖出去,送的是原文。
|
|
27
|
+
- **球上的数字**:单个会话的球显示这个会话本身的上下文大小(直接读自工具的记录);融合或压缩过的球显示拖出去要送的内容的 token 数,由本地分词器数出来,前面带 ≈。
|
|
28
|
+
- **名字和成分**:球下方写着里面的会话,前面带工具名;鼠标悬停在名字上显示成分,从列表里把某个会话拖出去就能拆开。
|
|
29
|
+
- **移动和移出**:把球拖到左边另一个池子就搬过去;拖出工作区就移出池子。
|
|
30
|
+
|
|
31
|
+
## 往上拖时会发生什么
|
|
32
|
+
|
|
33
|
+
GFT-Merge 在那个工具里新建会话(名为「merge · 池子名」),把内容作为第一条消息发进去,并请 AI 先简要汇报它的理解(在做什么、进展到哪、定了什么、没定什么、下一步),然后等你的指示。这是一轮真对话,所以新会话排在工具列表最上面。不装钩子、不改各工具的配置、不插手它们的处理过程。
|
|
34
|
+
|
|
35
|
+
- **Codex**:新建并发出后立刻返回;回复结束后用 Codex 桌面端自己的 `codex://` 链接打开这个会话。
|
|
36
|
+
- **Claude Code**:等 Claude 回完(几秒到一分钟左右,看内容长短),再用桌面端自己的 `claude://resume` 链接把会话导入桌面端并打开。
|
|
37
|
+
- 新会话放在来源会话最多的那个项目目录。
|
|
38
|
+
|
|
39
|
+
为什么不支持发进已有会话:Codex 会话在 Codex 桌面端里开着时,Codex 不让别的程序往里写;Claude 桌面端显示的是自己缓存的内容,别的程序写进会话文件的消息它不显示。两条路都靠不住,所以只做新建。
|
|
40
|
+
|
|
41
|
+
注意:
|
|
42
|
+
|
|
43
|
+
- 送的是精简原话:你说的话和 AI 每轮的最终回答,不含中间过程和工具日志。只有你点了压缩,才会调用模型、花你的额度。
|
|
44
|
+
- 没压缩的内容全文送;很长时会占满对方的上下文,可以先压缩。
|
|
45
|
+
- 目前只在 Windows 上实测过;macOS 和 Linux 上打开桌面端的链接没有验证。
|
|
46
|
+
|
|
47
|
+
## 命令
|
|
48
|
+
|
|
49
|
+
`open`(默认)· `serve` · `stop` · `status`,可加 `--port`。
|
|
50
|
+
|
|
51
|
+
## 数据
|
|
52
|
+
|
|
53
|
+
默认在 `~/.gft-merge/`(可用 `GFT_MERGE_HOME` 改):
|
|
54
|
+
|
|
55
|
+
- `pools/<id>.json`:池子名、里面的球、每个球里的会话和压缩块。
|
|
56
|
+
- `pools/<id>.jsonl`:池子里的消息,一行一条。被压缩的会话,原消息随之移除,只留压缩块;原会话本身在各工具里不受影响。
|
|
57
|
+
- `settings.json`:启用哪些工具、处理工具用哪个工具的哪个模型。
|
|
58
|
+
|
|
59
|
+
送出的内容以 `〔GFT-Merge〕` 开头;再次合并时会跳过这类消息,防止来回同步越滚越大。
|
|
60
|
+
|
|
61
|
+
## 开发
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install
|
|
65
|
+
npm test
|
|
66
|
+
node cli.mjs open
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 许可
|
|
70
|
+
|
|
71
|
+
MIT
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
const [command = 'open', ...rest] = process.argv.slice(2);
|
|
12
|
+
const port =
|
|
13
|
+
Number(rest[rest.indexOf('--port') + 1]) || Number(process.env.GFT_MERGE_PORT) || DEFAULT_PORT;
|
|
14
|
+
const url = `http://127.0.0.1:${port}`;
|
|
15
|
+
const serviceFile = path.join(homeDir(), 'service.json');
|
|
16
|
+
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
17
|
+
|
|
18
|
+
const portTaken = () =>
|
|
19
|
+
new Error(
|
|
20
|
+
`端口 ${port} 已被其他程序占用,GFT-Merge 没有启动。请换一个端口,例如:node cli.mjs open --port ${port + 10}`,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/** Whether anything listens on the port, without speaking HTTP to it. */
|
|
24
|
+
const listening = () =>
|
|
25
|
+
new Promise(resolve => {
|
|
26
|
+
const socket = connect({ host: '127.0.0.1', port }, () => {
|
|
27
|
+
socket.destroy();
|
|
28
|
+
resolve(true);
|
|
29
|
+
});
|
|
30
|
+
socket.once('error', () => resolve(false));
|
|
31
|
+
socket.setTimeout(1500, () => {
|
|
32
|
+
socket.destroy();
|
|
33
|
+
resolve(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** 'ours' when GFT-Merge answers on the port, 'other' when something else holds it, else 'free'. */
|
|
38
|
+
async function probe() {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(`${url}/api/state`, { signal: AbortSignal.timeout(1500) });
|
|
41
|
+
if (response.ok && (await response.json().catch(() => null))?.product === 'gft-merge')
|
|
42
|
+
return 'ours';
|
|
43
|
+
} catch {
|
|
44
|
+
// Not an HTTP answer from us; see whether the port is taken at all.
|
|
45
|
+
}
|
|
46
|
+
return (await listening()) ? 'other' : 'free';
|
|
47
|
+
}
|
|
48
|
+
const running = async () => (await probe()) === 'ours';
|
|
49
|
+
|
|
50
|
+
function openBrowser() {
|
|
51
|
+
const [file, args] =
|
|
52
|
+
process.platform === 'win32'
|
|
53
|
+
? ['cmd', ['/c', 'start', '', url]]
|
|
54
|
+
: [process.platform === 'darwin' ? 'open' : 'xdg-open', [url]];
|
|
55
|
+
spawn(file, args, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const commands = {
|
|
59
|
+
async serve() {
|
|
60
|
+
await startServer({ port }).catch(error => {
|
|
61
|
+
throw error.code === 'EADDRINUSE' ? portTaken() : error;
|
|
62
|
+
});
|
|
63
|
+
await writeJson(serviceFile, { pid: process.pid, port, startedAt: new Date().toISOString() });
|
|
64
|
+
console.log(`GFT-Merge: ${url}`);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async open() {
|
|
68
|
+
const state = await probe();
|
|
69
|
+
if (state === 'other') throw portTaken();
|
|
70
|
+
if (state === 'free') {
|
|
71
|
+
await mkdir(homeDir(), { recursive: true });
|
|
72
|
+
const log = await openFile(path.join(homeDir(), 'service.log'), 'a');
|
|
73
|
+
const script = fileURLToPath(import.meta.url);
|
|
74
|
+
spawn(process.execPath, [script, 'serve', '--port', String(port)], {
|
|
75
|
+
detached: true,
|
|
76
|
+
windowsHide: true,
|
|
77
|
+
stdio: ['ignore', log.fd, log.fd],
|
|
78
|
+
}).unref();
|
|
79
|
+
await log.close();
|
|
80
|
+
for (let i = 0; i < 50 && !(await running()); i++) await delay(100);
|
|
81
|
+
if (!(await running()))
|
|
82
|
+
throw new Error(`启动失败,请查看 ${path.join(homeDir(), 'service.log')}`);
|
|
83
|
+
}
|
|
84
|
+
openBrowser();
|
|
85
|
+
console.log(`GFT-Merge 已打开:${url}`);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/** Stops the process this data directory started, as recorded when it began serving. */
|
|
89
|
+
async stop() {
|
|
90
|
+
const service = await readJson(serviceFile);
|
|
91
|
+
const alive = pid => {
|
|
92
|
+
try {
|
|
93
|
+
process.kill(pid, 0);
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
if (!service?.pid || !alive(service.pid)) {
|
|
100
|
+
await rm(serviceFile, { force: true });
|
|
101
|
+
return console.log('GFT-Merge 没有在运行。');
|
|
102
|
+
}
|
|
103
|
+
process.kill(service.pid);
|
|
104
|
+
await rm(serviceFile, { force: true });
|
|
105
|
+
console.log('GFT-Merge 已停止。');
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async status() {
|
|
109
|
+
console.log(
|
|
110
|
+
JSON.stringify(
|
|
111
|
+
{
|
|
112
|
+
running: await running(),
|
|
113
|
+
url,
|
|
114
|
+
data: homeDir(),
|
|
115
|
+
},
|
|
116
|
+
null,
|
|
117
|
+
2,
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (!commands[command]) {
|
|
124
|
+
console.error(`用法:gft-merge ${Object.keys(commands).join(' | ')} [--port 端口]`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
await commands[command]();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(error.message);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
}
|