driftseal 0.4.0 → 0.5.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/README.md +33 -2
- package/README.zh-CN.md +31 -2
- package/bin/driftseal.js +225 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,12 +59,42 @@ Fix the server to one repository when starting it:
|
|
|
59
59
|
driftseal-mcp --root /absolute/path/to/repository
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Install the server into the current repository's agent config with one of the
|
|
63
|
+
supported targets:
|
|
63
64
|
|
|
64
65
|
```sh
|
|
65
|
-
|
|
66
|
+
cd /path/to/repository
|
|
67
|
+
driftseal mcp install --target codex
|
|
68
|
+
driftseal mcp install --target kimi-code
|
|
69
|
+
driftseal mcp install --target opencode
|
|
70
|
+
driftseal mcp install --target claude-code
|
|
71
|
+
driftseal mcp install --target cursor
|
|
66
72
|
```
|
|
67
73
|
|
|
74
|
+
Project scope is the default because each DriftSeal MCP server belongs to one
|
|
75
|
+
repository. Every target pins `--root` to the repository's canonical absolute
|
|
76
|
+
path, and repeated installs are idempotent.
|
|
77
|
+
|
|
78
|
+
| Target | Project config | Global config |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| `codex` | `.codex/config.toml` | `~/.codex/config.toml` |
|
|
81
|
+
| `kimi-code` | `.kimi-code/mcp.json` | `~/.kimi-code/mcp.json` or `$KIMI_CODE_HOME/mcp.json` |
|
|
82
|
+
| `opencode` | `opencode.json` | `~/.config/opencode/opencode.json` |
|
|
83
|
+
| `claude-code` | `.mcp.json` | `~/.claude.json` |
|
|
84
|
+
| `cursor` | `.cursor/mcp.json` | `~/.cursor/mcp.json` |
|
|
85
|
+
|
|
86
|
+
Use `--root <repository>` when running the installer elsewhere, or choose the
|
|
87
|
+
agent's user-level config explicitly:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
driftseal mcp install --target <target> --scope global --root /absolute/path/to/repository
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Global installs remain pinned to the selected repository. If the chosen config
|
|
94
|
+
already contains a different DriftSeal server entry, the installer leaves it
|
|
95
|
+
untouched unless `--force` is supplied. Other agent settings and MCP servers are
|
|
96
|
+
preserved.
|
|
97
|
+
|
|
68
98
|
The root is startup configuration, not a tool input. In MCP mode DriftSeal also
|
|
69
99
|
ignores inherited `DRIFTSEAL_HOME` and `DRIFTSEAL_DECISION_HOME` overrides, so a
|
|
70
100
|
tool call cannot redirect writes outside the selected repository.
|
|
@@ -121,6 +151,7 @@ Single-step commands that only build, check, or record work already done — com
|
|
|
121
151
|
| `driftseal decision update <id> [-s status] -n "..."` | Reconcile a linked decision in the open intent. |
|
|
122
152
|
| `driftseal decision list [-s status] [--last N \| --count]` | List or count decision records, optionally filtered by status. |
|
|
123
153
|
| `driftseal decision show <id>` | Read one decision record. |
|
|
154
|
+
| `driftseal mcp install --target TARGET [--scope project\|global] [--root path] [--force]` | Install the repository-pinned MCP server into Codex, Kimi Code, OpenCode, Claude Code, or Cursor. |
|
|
124
155
|
| `driftseal init` | Add the adoption protocol to `AGENTS.md`. |
|
|
125
156
|
| `driftseal help` | Print CLI usage. |
|
|
126
157
|
|
package/README.zh-CN.md
CHANGED
|
@@ -59,12 +59,40 @@ atomic write、schema 和 recovery 实现。server 不会启动 `driftseal` 子
|
|
|
59
59
|
driftseal-mcp --root /absolute/path/to/repository
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
选择对应 target,即可把 server 安装到当前 repository 的 agent 配置:
|
|
63
63
|
|
|
64
64
|
```sh
|
|
65
|
-
|
|
65
|
+
cd /path/to/repository
|
|
66
|
+
driftseal mcp install --target codex
|
|
67
|
+
driftseal mcp install --target kimi-code
|
|
68
|
+
driftseal mcp install --target opencode
|
|
69
|
+
driftseal mcp install --target claude-code
|
|
70
|
+
driftseal mcp install --target cursor
|
|
66
71
|
```
|
|
67
72
|
|
|
73
|
+
默认使用项目级配置,因为每个 DriftSeal MCP server 都只属于一个 repository。
|
|
74
|
+
所有 target 都会把 `--root` 固定为 repository 的规范化绝对路径,并且可以安全地
|
|
75
|
+
重复执行。
|
|
76
|
+
|
|
77
|
+
| Target | 项目级配置 | 全局配置 |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| `codex` | `.codex/config.toml` | `~/.codex/config.toml` |
|
|
80
|
+
| `kimi-code` | `.kimi-code/mcp.json` | `~/.kimi-code/mcp.json` 或 `$KIMI_CODE_HOME/mcp.json` |
|
|
81
|
+
| `opencode` | `opencode.json` | `~/.config/opencode/opencode.json` |
|
|
82
|
+
| `claude-code` | `.mcp.json` | `~/.claude.json` |
|
|
83
|
+
| `cursor` | `.cursor/mcp.json` | `~/.cursor/mcp.json` |
|
|
84
|
+
|
|
85
|
+
在其他目录执行时可以显式传入 `--root <repository>`;也可以明确选择对应 agent
|
|
86
|
+
的用户级配置:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
driftseal mcp install --target <target> --scope global --root /absolute/path/to/repository
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
全局安装仍会固定到所选 repository。如果目标配置中已经存在不同的 DriftSeal
|
|
93
|
+
server entry,安装器不会修改它;只有显式传入 `--force` 才会替换。其他 agent
|
|
94
|
+
设置与 MCP servers 会被保留。
|
|
95
|
+
|
|
68
96
|
root 只能在启动时配置,不是 tool input。MCP 模式也会忽略继承到进程中的
|
|
69
97
|
`DRIFTSEAL_HOME` 和 `DRIFTSEAL_DECISION_HOME` override,因此 tool call 不能把
|
|
70
98
|
写入重定向到所选 repository 之外。
|
|
@@ -121,6 +149,7 @@ driftseal end \
|
|
|
121
149
|
| `driftseal decision update <id> [-s status] -n "..."` | 在当前 intent 中 reconcile 已关联的 decision。 |
|
|
122
150
|
| `driftseal decision list [-s status] [--last N \| --count]` | 列出或统计 decision records,也可按 status 筛选。 |
|
|
123
151
|
| `driftseal decision show <id>` | 查看单条 decision record。 |
|
|
152
|
+
| `driftseal mcp install --target TARGET [--scope project\|global] [--root path] [--force]` | 把固定到 repository 的 MCP server 安装到 Codex、Kimi Code、OpenCode、Claude Code 或 Cursor。 |
|
|
124
153
|
| `driftseal init` | 把接入协议写入 `AGENTS.md`。 |
|
|
125
154
|
| `driftseal help` | 查看 CLI 用法。 |
|
|
126
155
|
|
package/bin/driftseal.js
CHANGED
|
@@ -1319,6 +1319,221 @@ function upgradeManagedBlock({
|
|
|
1319
1319
|
fail(`cannot safely upgrade customized protocol block beginning with ${marker}`);
|
|
1320
1320
|
}
|
|
1321
1321
|
|
|
1322
|
+
const MCP_TARGETS = ['codex', 'kimi-code', 'opencode', 'claude-code', 'cursor'];
|
|
1323
|
+
const MCP_SCOPES = ['project', 'global'];
|
|
1324
|
+
const MCP_TARGET_LABELS = {
|
|
1325
|
+
codex: 'Codex',
|
|
1326
|
+
'kimi-code': 'Kimi Code',
|
|
1327
|
+
opencode: 'OpenCode',
|
|
1328
|
+
'claude-code': 'Claude Code',
|
|
1329
|
+
cursor: 'Cursor',
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
function mcpInstallUsage() {
|
|
1333
|
+
return 'usage: driftseal mcp install --target <codex|kimi-code|opencode|claude-code|cursor> [--scope project|global] [--root <repository>] [--force]';
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function mcpConfigLocation(target, scope, root) {
|
|
1337
|
+
const home = os.homedir();
|
|
1338
|
+
if (target === 'codex') {
|
|
1339
|
+
const configDir = scope === 'project' ? path.join(root, '.codex') : path.join(home, '.codex');
|
|
1340
|
+
return { configDir, configFile: path.join(configDir, 'config.toml') };
|
|
1341
|
+
}
|
|
1342
|
+
if (target === 'kimi-code') {
|
|
1343
|
+
const userDir = process.env.KIMI_CODE_HOME
|
|
1344
|
+
? path.resolve(process.env.KIMI_CODE_HOME)
|
|
1345
|
+
: path.join(home, '.kimi-code');
|
|
1346
|
+
const configDir = scope === 'project' ? path.join(root, '.kimi-code') : userDir;
|
|
1347
|
+
return { configDir, configFile: path.join(configDir, 'mcp.json') };
|
|
1348
|
+
}
|
|
1349
|
+
if (target === 'opencode') {
|
|
1350
|
+
const configDir =
|
|
1351
|
+
scope === 'project' ? root : path.join(home, '.config', 'opencode');
|
|
1352
|
+
return { configDir, configFile: path.join(configDir, 'opencode.json') };
|
|
1353
|
+
}
|
|
1354
|
+
if (target === 'claude-code') {
|
|
1355
|
+
const configDir = scope === 'project' ? root : home;
|
|
1356
|
+
return {
|
|
1357
|
+
configDir,
|
|
1358
|
+
configFile: path.join(configDir, scope === 'project' ? '.mcp.json' : '.claude.json'),
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
if (target === 'cursor') {
|
|
1362
|
+
const configDir = scope === 'project' ? path.join(root, '.cursor') : path.join(home, '.cursor');
|
|
1363
|
+
return { configDir, configFile: path.join(configDir, 'mcp.json') };
|
|
1364
|
+
}
|
|
1365
|
+
fail(`unsupported MCP target "${target}"`);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function parseMcpInstallRequest(argv) {
|
|
1369
|
+
const [subcommand, ...rest] = argv;
|
|
1370
|
+
if (subcommand !== 'install') {
|
|
1371
|
+
fail(mcpInstallUsage());
|
|
1372
|
+
}
|
|
1373
|
+
const { positionals, flags } = parseArgs(rest, {
|
|
1374
|
+
target: 'single',
|
|
1375
|
+
scope: 'single',
|
|
1376
|
+
root: 'single',
|
|
1377
|
+
force: 'boolean',
|
|
1378
|
+
});
|
|
1379
|
+
if (positionals.length > 0 || !flags.target) {
|
|
1380
|
+
fail(mcpInstallUsage());
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
const target = flags.target.toLowerCase();
|
|
1384
|
+
if (!MCP_TARGETS.includes(target)) {
|
|
1385
|
+
fail(`unsupported MCP target "${flags.target}" (expected: ${MCP_TARGETS.join(', ')})`);
|
|
1386
|
+
}
|
|
1387
|
+
const scope = (flags.scope || 'project').toLowerCase();
|
|
1388
|
+
if (!MCP_SCOPES.includes(scope)) {
|
|
1389
|
+
fail(`invalid MCP install scope "${scope}" (expected: ${MCP_SCOPES.join(', ')})`);
|
|
1390
|
+
}
|
|
1391
|
+
const root = repositoryRoot(flags.root || process.cwd());
|
|
1392
|
+
const { configDir, configFile } = mcpConfigLocation(target, scope, root);
|
|
1393
|
+
return {
|
|
1394
|
+
target,
|
|
1395
|
+
targetLabel: MCP_TARGET_LABELS[target],
|
|
1396
|
+
scope,
|
|
1397
|
+
root,
|
|
1398
|
+
force: Boolean(flags.force),
|
|
1399
|
+
configDir,
|
|
1400
|
+
configFile,
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function tomlString(value) {
|
|
1405
|
+
return JSON.stringify(String(value));
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function codexMcpSection(root, eol = '\n') {
|
|
1409
|
+
return [
|
|
1410
|
+
'[mcp_servers.driftseal]',
|
|
1411
|
+
'command = "driftseal-mcp"',
|
|
1412
|
+
`args = ["--root", ${tomlString(root)}]`,
|
|
1413
|
+
].join(eol);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function codexMcpSectionRange(content) {
|
|
1417
|
+
const header = /^[ \t]*\[mcp_servers\.(?:driftseal|"driftseal"|'driftseal')\][ \t]*(?:#.*)?\r?$/gm;
|
|
1418
|
+
const matches = [...content.matchAll(header)];
|
|
1419
|
+
if (matches.length > 1) fail('Codex config contains duplicate mcp_servers.driftseal tables');
|
|
1420
|
+
if (matches.length === 0) return null;
|
|
1421
|
+
|
|
1422
|
+
const start = matches[0].index;
|
|
1423
|
+
const nextTable = /^[ \t]*\[[^\r\n]+\][ \t]*(?:#.*)?\r?$/gm;
|
|
1424
|
+
nextTable.lastIndex = start + matches[0][0].length;
|
|
1425
|
+
const next = nextTable.exec(content);
|
|
1426
|
+
return { start, end: next ? next.index : content.length };
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function installCodexMcp(request) {
|
|
1430
|
+
const { configDir, configFile, force, root, scope, target, targetLabel } = request;
|
|
1431
|
+
const existed = fs.existsSync(configFile);
|
|
1432
|
+
const current = existed ? fs.readFileSync(configFile, 'utf8') : '';
|
|
1433
|
+
const eol = current.includes('\r\n') ? '\r\n' : '\n';
|
|
1434
|
+
const section = codexMcpSection(root, eol);
|
|
1435
|
+
const range = codexMcpSectionRange(current);
|
|
1436
|
+
let updated;
|
|
1437
|
+
|
|
1438
|
+
if (!range) {
|
|
1439
|
+
const separator =
|
|
1440
|
+
current.length === 0
|
|
1441
|
+
? ''
|
|
1442
|
+
: current.endsWith(eol + eol)
|
|
1443
|
+
? ''
|
|
1444
|
+
: current.endsWith(eol)
|
|
1445
|
+
? eol
|
|
1446
|
+
: eol + eol;
|
|
1447
|
+
updated = current + separator + section + eol;
|
|
1448
|
+
} else {
|
|
1449
|
+
const existingSection = current.slice(range.start, range.end).trim();
|
|
1450
|
+
if (existingSection.replace(/\r\n/g, '\n') === section.replace(/\r\n/g, '\n')) {
|
|
1451
|
+
printLine(`DriftSeal MCP is already installed for ${targetLabel} (${scope}): ${configFile}`);
|
|
1452
|
+
return { changed: false, target, scope, root, configFile };
|
|
1453
|
+
}
|
|
1454
|
+
if (!force) {
|
|
1455
|
+
fail(
|
|
1456
|
+
`Codex config already defines mcp_servers.driftseal in ${configFile}; ` +
|
|
1457
|
+
're-run with --force to replace that table'
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
const trailing = range.end < current.length ? eol + eol : eol;
|
|
1461
|
+
updated = current.slice(0, range.start) + section + trailing + current.slice(range.end);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
ensureDirectoryDurable(configDir);
|
|
1465
|
+
atomicWriteFile(configFile, updated);
|
|
1466
|
+
printLine(`Installed DriftSeal MCP for ${targetLabel} (${scope}): ${configFile}`);
|
|
1467
|
+
printLine(`Repository root: ${root}`);
|
|
1468
|
+
return { changed: true, target, scope, root, configFile };
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function jsonObject(value) {
|
|
1472
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function readJsonConfig(configFile, targetLabel, target) {
|
|
1476
|
+
if (!fs.existsSync(configFile)) {
|
|
1477
|
+
return target === 'opencode' ? { $schema: 'https://opencode.ai/config.json' } : {};
|
|
1478
|
+
}
|
|
1479
|
+
let parsed;
|
|
1480
|
+
try {
|
|
1481
|
+
parsed = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
1482
|
+
} catch {
|
|
1483
|
+
fail(`${targetLabel} config is not valid JSON: ${configFile}`);
|
|
1484
|
+
}
|
|
1485
|
+
if (!jsonObject(parsed)) fail(`${targetLabel} config must contain a JSON object: ${configFile}`);
|
|
1486
|
+
return parsed;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function jsonMcpDefinition(target, root) {
|
|
1490
|
+
if (target === 'opencode') {
|
|
1491
|
+
return {
|
|
1492
|
+
containerKey: 'mcp',
|
|
1493
|
+
server: { type: 'local', command: ['driftseal-mcp', '--root', root] },
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
return {
|
|
1497
|
+
containerKey: 'mcpServers',
|
|
1498
|
+
server: { command: 'driftseal-mcp', args: ['--root', root] },
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function installJsonMcp(request) {
|
|
1503
|
+
const { configDir, configFile, force, root, scope, target, targetLabel } = request;
|
|
1504
|
+
const config = readJsonConfig(configFile, targetLabel, target);
|
|
1505
|
+
const { containerKey, server } = jsonMcpDefinition(target, root);
|
|
1506
|
+
if (config[containerKey] === undefined) config[containerKey] = {};
|
|
1507
|
+
if (!jsonObject(config[containerKey])) {
|
|
1508
|
+
fail(`${targetLabel} config field ${containerKey} must be a JSON object: ${configFile}`);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
const existing = config[containerKey].driftseal;
|
|
1512
|
+
if (existing !== undefined) {
|
|
1513
|
+
if (JSON.stringify(existing) === JSON.stringify(server)) {
|
|
1514
|
+
printLine(`DriftSeal MCP is already installed for ${targetLabel} (${scope}): ${configFile}`);
|
|
1515
|
+
return { changed: false, target, scope, root, configFile };
|
|
1516
|
+
}
|
|
1517
|
+
if (!force) {
|
|
1518
|
+
fail(
|
|
1519
|
+
`${targetLabel} config already defines the driftseal MCP server in ${configFile}; ` +
|
|
1520
|
+
're-run with --force to replace that entry'
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
config[containerKey].driftseal = server;
|
|
1526
|
+
ensureDirectoryDurable(configDir);
|
|
1527
|
+
atomicWriteFile(configFile, JSON.stringify(config, null, 2) + '\n');
|
|
1528
|
+
printLine(`Installed DriftSeal MCP for ${targetLabel} (${scope}): ${configFile}`);
|
|
1529
|
+
printLine(`Repository root: ${root}`);
|
|
1530
|
+
return { changed: true, target, scope, root, configFile };
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function installMcp(request) {
|
|
1534
|
+
return request.target === 'codex' ? installCodexMcp(request) : installJsonMcp(request);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1322
1537
|
const commands = {
|
|
1323
1538
|
begin(argv) {
|
|
1324
1539
|
const { positionals, flags } = parseArgs(argv, {
|
|
@@ -1719,6 +1934,11 @@ const commands = {
|
|
|
1719
1934
|
fail('usage: driftseal decision add|update|list|show (run: driftseal help)');
|
|
1720
1935
|
},
|
|
1721
1936
|
|
|
1937
|
+
mcp(argv) {
|
|
1938
|
+
const request = parseMcpInstallRequest(argv);
|
|
1939
|
+
return installMcp(request);
|
|
1940
|
+
},
|
|
1941
|
+
|
|
1722
1942
|
init(argv) {
|
|
1723
1943
|
const { positionals } = parseArgs(argv, {});
|
|
1724
1944
|
if (positionals.length > 0) fail('usage: driftseal init');
|
|
@@ -1805,6 +2025,9 @@ usage:
|
|
|
1805
2025
|
driftseal decision list [--status STATUS] [--last N | --count]
|
|
1806
2026
|
list or count filtered MADR decision records
|
|
1807
2027
|
driftseal decision show <id> print one MADR decision record
|
|
2028
|
+
driftseal mcp install --target TARGET [--scope project|global] [--root <repository>] [--force]
|
|
2029
|
+
install the repository-pinned MCP server (default: project)
|
|
2030
|
+
targets: codex, kimi-code, opencode, claude-code, cursor
|
|
1808
2031
|
driftseal init inject intent and decision protocols into ./AGENTS.md
|
|
1809
2032
|
driftseal help
|
|
1810
2033
|
|
|
@@ -1829,6 +2052,7 @@ function requestedEndStatus(argv) {
|
|
|
1829
2052
|
}
|
|
1830
2053
|
|
|
1831
2054
|
function mutationResources(cmd, argv) {
|
|
2055
|
+
if (cmd === 'mcp') return [parseMcpInstallRequest(argv).configDir];
|
|
1832
2056
|
if (cmd === 'init') return [process.cwd()];
|
|
1833
2057
|
if (cmd === 'reclaim' || cmd === 'unreclaim') return [logDir()];
|
|
1834
2058
|
if (cmd === 'begin' && !argv.some((arg) => arg === '--decision' || arg.startsWith('--decision='))) {
|
|
@@ -1848,7 +2072,7 @@ function dispatch(argv) {
|
|
|
1848
2072
|
const fn = commands[cmd];
|
|
1849
2073
|
if (!fn) fail(`unknown command: ${cmd} (run: driftseal help)`);
|
|
1850
2074
|
const mutates =
|
|
1851
|
-
['begin', 'end', 'init', 'reclaim', 'unreclaim'].includes(cmd) ||
|
|
2075
|
+
['begin', 'end', 'init', 'mcp', 'reclaim', 'unreclaim'].includes(cmd) ||
|
|
1852
2076
|
(cmd === 'decision' && ['add', 'update'].includes(rest[0]));
|
|
1853
2077
|
const readsIntentLog = ['status', 'log'].includes(cmd);
|
|
1854
2078
|
if (mutates || readsIntentLog) {
|