@useshifu/coding-harness 0.3.16 → 0.3.17

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
@@ -104,7 +104,7 @@ Missing local turns are reported in `unavailableSegments`. A partial overlap wit
104
104
  The context branch adds a remote MCP connection using the same saved Codex key:
105
105
 
106
106
  ```sh
107
- npx --yes @useshifu/coding-harness@0.3.16 mcp-setup --harness codex --mcp-url https://YOUR-BRANCH-HOST/mcp
107
+ npx --yes @useshifu/coding-harness@0.3.17 mcp-setup --harness codex --mcp-url https://YOUR-BRANCH-HOST/mcp
108
108
  ```
109
109
 
110
110
  Before this release is published, install dependencies and run from the checkout root:
@@ -133,13 +133,29 @@ Official configuration reference: https://learn.chatgpt.com/docs/config-file/con
133
133
  OpenCode uses the same `/mcp` endpoint and three tools, authenticated with its own saved OpenCode connection key:
134
134
 
135
135
  ```sh
136
- npx --yes @useshifu/coding-harness@0.3.16 mcp-setup --harness opencode --mcp-url https://YOUR-SHIFU-HOST/mcp
136
+ npx --yes @useshifu/coding-harness@0.3.17 mcp-setup --harness opencode --mcp-url https://YOUR-SHIFU-HOST/mcp
137
137
  ```
138
138
 
139
- Until 0.3.16 is published, install dependencies with `npm ci --prefix packages/shifu-coding-harness --ignore-scripts` from the checkout root, then run the checkout CLI with the same arguments. Setup preserves existing sync settings and other MCP servers, checks authenticated initialization and tool discovery, and installs the `shifu-context` usage instructions. A missing OpenCode connection prompts for its key in the terminal. A Codex key cannot authorize an OpenCode connection.
139
+ Until 0.3.17 is published, install dependencies with `npm ci --prefix packages/shifu-coding-harness --ignore-scripts` from the checkout root, then run the checkout CLI with the same arguments. Setup preserves existing sync settings and other MCP servers, checks authenticated initialization and tool discovery, and installs the `shifu-context` usage instructions. A missing OpenCode connection prompts for its key in the terminal. A Codex key cannot authorize an OpenCode connection.
140
140
 
141
141
  The installer adds a remote `shifu` entry with `oauth: false` to OpenCode's JSON or JSONC configuration, preserving existing content and comments. Its Authorization header uses OpenCode's `{file:...}` substitution to read a protected local credential file; the key stays out of configuration, command arguments and chat. Replacing the saved connection key refreshes that file; disconnect removes it. Restart OpenCode and start a new session after setup. Project-specific configuration can override global MCP settings; inspect `opencode mcp list` in the project if tools do not appear.
142
142
 
143
143
  The API derives the harness and tenant from the authenticated connector key. Memory updates from OpenCode carry `opencode` provenance; personal eligibility, general retrieval and evidence verification use the same rules as Codex. Coding-session sync remains separate. No additional local MCP server or OpenCode plugin is required.
144
144
 
145
145
  Official references: [MCP servers](https://docs.opencode.ai/docs/mcp-servers/) and [configuration](https://docs.opencode.ai/docs/config/).
146
+
147
+ ## Claude Code context and memory
148
+
149
+ Claude Code connects to the same `/mcp` endpoint and three tools with its own saved connection key:
150
+
151
+ ```sh
152
+ npx --yes @useshifu/coding-harness@0.3.17 mcp-setup --harness claude_code --mcp-url https://YOUR-SHIFU-HOST/mcp
153
+ ```
154
+
155
+ Until 0.3.17 is published, install checkout dependencies as above and run the checkout CLI with these arguments. Setup checks the existing Claude Code key, authenticated initialization and tool discovery, and installs the `shifu-context` skill. It preserves the sync endpoint, schedule, checkpoints and other MCP servers. A missing connection prompts for a Claude Code key in the terminal; do not substitute a Codex or OpenCode key.
156
+
157
+ The user-scope HTTP connection uses Claude Code's `headersHelper` to read the protected local Shifu connection file. The key is absent from MCP configuration and command arguments. Replacing the saved key is picked up when the helper runs on reconnection. Restart Claude Code or start a new session after setup; use `/mcp` to inspect or reconnect Shifu. Local or project MCP configuration can override a user-scope server with the same name.
158
+
159
+ The API records memory provenance as `claude_code` from the authenticated key. Context retrieval and memory behavior match Codex and OpenCode; ordinary session sync continues independently. This setup requires local macOS/Linux and a Claude Code CLI supporting HTTP `headersHelper`, verified with Claude Code 2.1.295. Unsupported or broken older clients need an update using Claude Code's official installation instructions; there is no plaintext credential fallback. `CLAUDE_CONFIG_DIR` is honored for MCP configuration, installed skills and session discovery.
160
+
161
+ Official reference: [Claude Code MCP connections](https://code.claude.com/docs/en/mcp).
@@ -40,7 +40,7 @@ const MAX_TURNS_PER_SEGMENT = 12;
40
40
  const MAX_TITLE_LENGTH = 160;
41
41
  const MAX_WORK_DETAIL_LENGTH = 600;
42
42
  const CONFIG_VERSION = 2;
43
- const RUNNER_VERSION = "0.3.16";
43
+ const RUNNER_VERSION = "0.3.17";
44
44
  const REDACTION_VERSION = 4;
45
45
  const DEFAULT_INTERVAL_HOURS = 1;
46
46
  const LEGACY_INTERVAL_HOURS = 4;
@@ -94,7 +94,7 @@ function codexSessionsRoot() {
94
94
  }
95
95
 
96
96
  function claudeSessionsRoot() {
97
- return process.env.SHIFU_CLAUDE_SESSIONS_ROOT || path.join(os.homedir(), ".claude", "projects");
97
+ return process.env.SHIFU_CLAUDE_SESSIONS_ROOT || path.join(claudeHome(), "projects");
98
98
  }
99
99
 
100
100
  function opencodeQuery(query) {
@@ -1085,7 +1085,7 @@ function removeCodexHooks() {
1085
1085
 
1086
1086
  function installDestination(harness) {
1087
1087
  if (harness === "codex") return path.join(codexHome(), "skills", "shifu-sync");
1088
- if (harness === "claude_code") return path.join(os.homedir(), ".claude", "skills", "shifu-sync");
1088
+ if (harness === "claude_code") return path.join(claudeHome(), "skills", "shifu-sync");
1089
1089
  return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode", "skills", "shifu-sync");
1090
1090
  }
1091
1091
 
@@ -1120,7 +1120,7 @@ function install(harness, quiet = false) {
1120
1120
  .replaceAll("npx --yes @useshifu/coding-harness", `"${process.execPath}" "${installedRunnerPath()}"`);
1121
1121
  fs.writeFileSync(skill, content, { mode: 0o600 });
1122
1122
  }
1123
- if (harness === "codex" || harness === "opencode") {
1123
+ if (Object.hasOwn(HARNESS_NAMES, harness)) {
1124
1124
  const contextDestination = path.join(path.dirname(installDestination(harness)), "shifu-context");
1125
1125
  copyDirectory(path.join(__dirname, "..", "skills", "shifu-context"), contextDestination);
1126
1126
  const contextFile = path.join(contextDestination, "SKILL.md");
@@ -1310,8 +1310,12 @@ function codexHome() {
1310
1310
  return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
1311
1311
  }
1312
1312
 
1313
+ function claudeHome() {
1314
+ return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
1315
+ }
1316
+
1313
1317
  function mcpHeaders(harness) {
1314
- if (harness !== "codex" && harness !== "opencode") throw new Error("Shifu MCP currently supports Codex and OpenCode.");
1318
+ requireHarness(harness);
1315
1319
  const file = configPath(harness);
1316
1320
  const stat = fs.lstatSync(file);
1317
1321
  if (!stat.isFile() || (process.platform !== "win32" && ((stat.mode & 0o077) || stat.uid !== process.getuid()))) {
@@ -1322,8 +1326,8 @@ function mcpHeaders(harness) {
1322
1326
  return { Authorization: `Bearer ${config.token}` };
1323
1327
  }
1324
1328
 
1325
- function helperCommand() {
1326
- const args = [process.execPath, installedRunnerPath(), "mcp-headers", "--harness", "codex", "--config-root", path.resolve(configRoot())];
1329
+ function helperCommand(harness = "codex") {
1330
+ const args = [process.execPath, path.resolve(installedRunnerPath()), "mcp-headers", "--harness", harness, "--config-root", path.resolve(configRoot())];
1327
1331
  if (process.platform === "win32") throw new Error("Prompt-driven MCP credential setup currently supports macOS and Linux. Windows requires a supported local credential-helper launcher.");
1328
1332
  return args.map((value) => `'${value.replaceAll("'", "'\\''")}'`).join(" ");
1329
1333
  }
@@ -1415,7 +1419,8 @@ async function verifyMcp(mcpUrl, token) {
1415
1419
 
1416
1420
  function prepareCodexMcpConfig(mcpUrl, helper) {
1417
1421
  const file = path.join(codexHome(), "config.toml");
1418
- const original = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
1422
+ const exists = fs.existsSync(file);
1423
+ const original = exists ? fs.readFileSync(file, "utf8") : "";
1419
1424
  const temporaryHome = fs.mkdtempSync(path.join(os.tmpdir(), "shifu-codex-mcp-"));
1420
1425
  const candidateFile = path.join(temporaryHome, "config.toml");
1421
1426
  const environment = { ...process.env, CODEX_HOME: temporaryHome };
@@ -1432,7 +1437,7 @@ function prepareCodexMcpConfig(mcpUrl, helper) {
1432
1437
  if (!parsed.transport?.http_headers_helper || parsed.transport.type !== "streamable_http" || parsed.transport.url !== mcpUrl) {
1433
1438
  throw new Error("This Codex version does not support local HTTP header helpers. Update Codex before MCP setup.");
1434
1439
  }
1435
- return { file, original, candidate };
1440
+ return { file, exists, original, candidate };
1436
1441
  } catch (error) {
1437
1442
  if (error?.code === "ENOENT") throw new Error("The codex command is unavailable. Install Codex CLI before MCP setup.");
1438
1443
  if (error instanceof Error && !error.stdout && !error.stderr) throw error;
@@ -1542,10 +1547,66 @@ function removeOpenCodeMcpConfig() {
1542
1547
  fs.rmSync(openCodeTokenPath(), { force: true });
1543
1548
  }
1544
1549
 
1550
+ function claudeMcpConfig() {
1551
+ const file = process.env.CLAUDE_CONFIG_DIR ? path.resolve(claudeHome(), ".claude.json") : path.join(os.homedir(), ".claude.json");
1552
+ const exists = fs.existsSync(file);
1553
+ if (exists && !fs.lstatSync(file).isFile()) throw new Error("Claude Code configuration must be a regular file, not a link. Nothing was changed.");
1554
+ const original = exists ? fs.readFileSync(file, "utf8") : "";
1555
+ let value;
1556
+ try { value = JSON.parse(original || "{}"); }
1557
+ catch { throw new Error("Claude Code configuration is invalid. Fix its JSON before MCP setup; nothing was changed."); }
1558
+ if (!value || typeof value !== "object" || Array.isArray(value) || (value.mcpServers !== undefined && (!value.mcpServers || typeof value.mcpServers !== "object" || Array.isArray(value.mcpServers)))) {
1559
+ throw new Error("Claude Code configuration is invalid. Nothing was changed.");
1560
+ }
1561
+ return { file, exists, original, value };
1562
+ }
1563
+
1564
+ function editClaudeMcp(original, entry) {
1565
+ return jsonc.applyEdits(original || "{}\n", jsonc.modify(original || "{}\n", ["mcpServers", "shifu"], entry, { formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" } }));
1566
+ }
1567
+
1568
+ function prepareClaudeMcpConfig(mcpUrl, helper) {
1569
+ const config = claudeMcpConfig();
1570
+ const entry = { type: "http", url: mcpUrl, headersHelper: helper, timeout: 120_000 };
1571
+ const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "shifu-claude-mcp-"));
1572
+ const environment = { ...process.env, HOME: temporary, CLAUDE_CONFIG_DIR: temporary, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1" };
1573
+ delete environment.CLAUDECODE;
1574
+ delete environment.CLAUDE_CODE_SESSION_ID;
1575
+ try {
1576
+ const claude = process.env.SHIFU_CLAUDE_BIN || "claude";
1577
+ const version = execFileSync(claude, ["--version"], { cwd: temporary, env: environment, stdio: ["ignore", "pipe", "pipe"], encoding: "utf8", timeout: 10_000, maxBuffer: 1024 * 1024 }).match(/^(\d+\.\d+\.\d+)\b/m)?.[1];
1578
+ if (!version || olderPackageVersion(version, "1.0.119")) throw new Error("Claude Code predates HTTP credential-helper support.");
1579
+ const temporaryConfig = path.join(temporary, ".claude.json");
1580
+ // Validate through Claude without letting its incidental settings writes replace ours.
1581
+ fs.writeFileSync(temporaryConfig, editClaudeMcp(config.original, undefined), { mode: 0o600 });
1582
+ execFileSync(claude, ["mcp", "add-json", "shifu", JSON.stringify(entry), "--scope", "user"], { cwd: temporary, env: environment, stdio: ["ignore", "pipe", "pipe"], timeout: 20_000, maxBuffer: 1024 * 1024 });
1583
+ const accepted = JSON.parse(fs.readFileSync(temporaryConfig, "utf8")).mcpServers?.shifu;
1584
+ if (accepted?.type !== entry.type || accepted.url !== mcpUrl || accepted.headersHelper !== helper || accepted.timeout !== entry.timeout) throw new Error("Claude Code did not accept the expected MCP credential helper.");
1585
+ return { ...config, candidate: editClaudeMcp(config.original, entry) };
1586
+ } catch (error) {
1587
+ if (error?.code === "ENOENT") throw new Error("The claude command is unavailable. Install Claude Code before MCP setup.");
1588
+ // Native diagnostics can include settings and credentials from other servers.
1589
+ throw new Error("Claude Code could not validate its HTTP MCP credential helper. Update Claude Code or fix its existing config; nothing was changed.");
1590
+ } finally {
1591
+ fs.rmSync(temporary, { recursive: true, force: true });
1592
+ }
1593
+ }
1594
+
1595
+ function removeClaudeMcpConfig(expectedHelper) {
1596
+ const config = claudeMcpConfig();
1597
+ if (config.value.mcpServers?.shifu?.headersHelper !== expectedHelper) return;
1598
+ if (!unchangedConfig(config)) throw new Error("Claude Code configuration changed during disconnect. Retry to preserve those changes.");
1599
+ writePrivateText(config.file, editClaudeMcp(config.original, undefined));
1600
+ }
1601
+
1602
+ function unchangedMcpConfig(prepared) {
1603
+ return (prepared.configs || [prepared]).every(unchangedConfig);
1604
+ }
1605
+
1545
1606
  async function setupMcp(harness) {
1546
- if (harness !== "codex" && harness !== "opencode") throw new Error("Shifu MCP currently supports Codex and OpenCode.");
1607
+ requireHarness(harness);
1547
1608
  if (process.platform !== "darwin" && process.platform !== "linux") throw new Error("Prompt-driven MCP credential setup currently supports local macOS and Linux.");
1548
- const helper = harness === "codex" ? helperCommand() : undefined;
1609
+ const helper = harness === "opencode" ? undefined : helperCommand(harness);
1549
1610
  if (fs.existsSync(configPath(harness)) && !fs.lstatSync(configPath(harness)).isFile()) {
1550
1611
  throw new Error(`The saved ${HARNESS_NAMES[harness]} connection must be a regular private file, not a link.`);
1551
1612
  }
@@ -1562,8 +1623,8 @@ async function setupMcp(harness) {
1562
1623
  if (!TOKEN_PATTERN.test(config.token)) throw new Error(`The saved ${HARNESS_NAMES[harness]} connection key is invalid. Run connect again.`);
1563
1624
  await validateConnection(config.apiUrl, config.token, harness);
1564
1625
  const tools = await verifyMcp(mcpUrl, config.token);
1565
- const prepared = harness === "codex" ? prepareCodexMcpConfig(mcpUrl, helper) : prepareOpenCodeMcpConfig(mcpUrl);
1566
- if (harness === "opencode" ? prepared.configs.some((config) => !unchangedConfig(config)) : (fs.existsSync(prepared.file) ? fs.readFileSync(prepared.file, "utf8") : "") !== prepared.original) {
1626
+ const prepared = harness === "codex" ? prepareCodexMcpConfig(mcpUrl, helper) : harness === "opencode" ? prepareOpenCodeMcpConfig(mcpUrl) : prepareClaudeMcpConfig(mcpUrl, helper);
1627
+ if (!unchangedMcpConfig(prepared)) {
1567
1628
  throw new Error(`${HARNESS_NAMES[harness]} configuration changed during setup. Run mcp-setup again to preserve those changes.`);
1568
1629
  }
1569
1630
  fs.chmodSync(configPath(harness), 0o600);
@@ -1573,12 +1634,12 @@ async function setupMcp(harness) {
1573
1634
  throw new Error("The shifu-context skill is missing. Run mcp-setup from the package or branch checkout to install it.");
1574
1635
  }
1575
1636
  if (fs.readFileSync(configPath(harness), "utf8") !== originalConnection) throw new Error("The saved Shifu connection changed during setup. Run mcp-setup again to preserve those changes.");
1576
- if (harness === "opencode" && prepared.configs.some((config) => !unchangedConfig(config))) throw new Error("OpenCode configuration changed during setup. Run mcp-setup again to preserve those changes.");
1637
+ if (!unchangedMcpConfig(prepared)) throw new Error(`${HARNESS_NAMES[harness]} configuration changed during setup. Run mcp-setup again to preserve those changes.`);
1577
1638
  if (harness === "opencode") writePrivateText(openCodeTokenPath(), config.token);
1578
1639
  writePrivateText(prepared.file, prepared.candidate);
1579
- writeJSON(configPath(harness), { ...config, mcpUrl });
1640
+ writeJSON(configPath(harness), { ...config, mcpUrl, ...(harness === "claude_code" ? { mcpHeadersHelper: helper } : {}) });
1580
1641
  console.error(`Verified Shifu MCP tools: ${tools.join(", ")}. ${HARNESS_NAMES[harness]} configuration and usage instructions are installed.`);
1581
- console.error(`The existing connection key is read ${harness === "codex" ? "by a local credential helper" : "from a private credential file"}; it was not copied into MCP configuration. The sync endpoint and schedule were preserved.`);
1642
+ console.error(`The existing connection key is read ${harness === "opencode" ? "from a private credential file" : "by a local credential helper"}; it was not copied into MCP configuration. The sync endpoint and schedule were preserved.`);
1582
1643
  console.error(`Reload ${HARNESS_NAMES[harness]} or start a new chat to load the MCP tools and shifu-context skill. This setup check does not prove the current chat has loaded them.`);
1583
1644
  }
1584
1645
 
@@ -1586,6 +1647,7 @@ function disconnect(harness) {
1586
1647
  const config = readConfig(harness);
1587
1648
  if (fs.existsSync(lockPath(harness))) throw new Error("A Shifu sync is running. Try disconnecting again after it finishes.");
1588
1649
  if (harness === "opencode" && (config.mcpUrl || fs.existsSync(openCodeTokenPath()))) removeOpenCodeMcpConfig();
1650
+ if (harness === "claude_code" && config.mcpUrl) removeClaudeMcpConfig(config.mcpHeadersHelper || helperCommand("claude_code"));
1589
1651
  removeSchedule(harness);
1590
1652
  for (const file of [configPath(harness), statePath(harness), pendingPath(harness)]) {
1591
1653
  try { fs.unlinkSync(file); } catch (error) { if (error?.code !== "ENOENT") throw error; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useshifu/coding-harness",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "description": "Scheduled, redacted coding-harness activity sync for Shifu",
5
5
  "bin": {
6
6
  "coding-harness": "bin/shifu-harness.js",
@@ -13,9 +13,9 @@ When asked to connect Shifu context, run:
13
13
  npx --yes @useshifu/coding-harness mcp-setup --harness codex
14
14
  ```
15
15
 
16
- For a branch deployment, append `--mcp-url` with the user's supplied HTTPS endpoint ending in `/mcp`. Keep the existing sync API endpoint. The CLI reuses this harness's saved key, checks the authenticated tools, and preserves existing settings. Codex uses a local credential helper; OpenCode uses its native `{file:...}` substitution pointing to a private credential file. The key is never placed literally in MCP configuration. If no connection exists, the CLI asks for this harness's key in the terminal. Never request or print the key in chat or place it in command arguments. When input is unavailable, have the user run the command in an interactive terminal. Do not substitute plaintext credentials for an unsupported credential mechanism.
16
+ For a branch deployment, append `--mcp-url` with the user's supplied HTTPS endpoint ending in `/mcp`. Keep the existing sync API endpoint. The CLI reuses this harness's saved key, checks the authenticated tools, and preserves existing settings. Codex and Claude Code use local credential helpers; OpenCode uses its native `{file:...}` substitution pointing to a private credential file. The key is never placed literally in MCP configuration. If no connection exists, the CLI asks for this harness's key in the terminal. Never request or print the key in chat or place it in command arguments. When input is unavailable, have the user run the command in an interactive terminal. Do not substitute plaintext credentials for an unsupported credential mechanism.
17
17
 
18
- Reload the harness or start a new chat after setup. Check that the tools are actually available before claiming this chat can use them. Setup supports local macOS/Linux connections; remote execution environments cannot read this device's credential helper or file. OpenCode requires the v1 config schema (verified with 1.18.30); unsupported versions or config schemas fail before MCP settings change.
18
+ Reload the harness or start a new chat after setup. Check that the tools are actually available before claiming this chat can use them. Setup supports local macOS/Linux connections; remote execution environments cannot read this device's credential helper or file. OpenCode requires the v1 config schema (verified with 1.18.30). Claude Code requires HTTP MCP with `headersHelper` (verified with 2.1.295); setup validates it through the installed CLI. If Claude Code is outdated, help the user update it using official installation instructions before retrying. The connection uses user scope and honors `CLAUDE_CONFIG_DIR`. Restart Claude Code and start a new session, or use `/mcp` to reconnect after rotating the saved key. Unsupported versions or config schemas fail before MCP settings change.
19
19
 
20
20
  ## Consult context
21
21