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 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
- For Codex, add the installed command as a stdio MCP server:
62
+ Install the server into the current repository's agent config with one of the
63
+ supported targets:
63
64
 
64
65
  ```sh
65
- codex mcp add driftseal -- driftseal-mcp --root /absolute/path/to/repository
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
- Codex 中,可以把安装后的命令添加为 stdio MCP server:
62
+ 选择对应 target,即可把 server 安装到当前 repository 的 agent 配置:
63
63
 
64
64
  ```sh
65
- codex mcp add driftseal -- driftseal-mcp --root /absolute/path/to/repository
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driftseal",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Seal intent, verification, and decisions into an auditable workflow for agentic coding",
5
5
  "keywords": [
6
6
  "driftseal",