hunter-harness 0.2.10 → 0.2.12

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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/bin.js +309 -80
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -21,8 +21,8 @@ npx hunter-harness update
21
21
  | Claude Code | `.claude/skills/` | `.claude/rules/*.md` | `.claude/agents/` |
22
22
  | Codex | `.agents/skills/` | `AGENTS.md` | 不生成 |
23
23
  | Cursor | `.cursor/skills/` | `.cursor/rules/*.mdc` | 不生成 |
24
- | CodeBuddy `both` | `.codebuddy/skills/` | `CODEBUDDY.md` | `.codebuddy/agents/` |
24
+ | CodeBuddy `both` | `.codebuddy/skills/` | `CODEBUDDY.md` + `.codebuddy/.rules/*.mdc` + `.codebuddy/rules/*.md` | `.codebuddy/agents/` |
25
25
 
26
- 需要 Node.js 24 或更高版本。token 只通过 `--token-env` 指定的环境变量读取,不要写入项目文件或命令参数。
26
+ 需要 Node.js 22.12 或更高版本。选择 CodeBuddy 时,可将已有 `.claude/rules` 非破坏性同步到 CodeBuddy,并在检测到 `.codegraph/` 时合并项目级 `.mcp.json`;疑似凭据内容会跳过。token 只通过 `--token-env` 指定的环境变量读取,不要写入项目文件或命令参数。
27
27
 
28
- 本包当前为 `UNLICENSED`。参考迁移资产的上游 license、commit/tag 和允许再分发范围确认前,不得对外发布。
28
+ 本包使用 MIT License。
package/dist/bin.js CHANGED
@@ -1098,8 +1098,8 @@ async function assertNoSymlinks(root, relativePath) {
1098
1098
  for (const segment of normalized.split("/")) {
1099
1099
  current = join(current, segment);
1100
1100
  try {
1101
- const stat6 = await lstat(current);
1102
- if (stat6.isSymbolicLink()) {
1101
+ const stat7 = await lstat(current);
1102
+ if (stat7.isSymbolicLink()) {
1103
1103
  throw new UnsafePathError("symbolic links are not managed");
1104
1104
  }
1105
1105
  } catch (error) {
@@ -1230,6 +1230,9 @@ var HunterHarnessApiClient = class {
1230
1230
  body
1231
1231
  });
1232
1232
  }
1233
+ async getProject(projectId, requestId) {
1234
+ return this.request("GET", "/api/v1/projects/" + encodeURIComponent(projectId), { requestId });
1235
+ }
1233
1236
  async createProposalSession(projectId, body, requestId, idempotencyKey) {
1234
1237
  return this.request("POST", "/api/v1/projects/" + encodeURIComponent(projectId) + "/proposal-sessions", { requestId, idempotencyKey, body });
1235
1238
  }
@@ -1982,6 +1985,13 @@ function makeAdapter(spec) {
1982
1985
  return projectBundleFiles(bundle, spec.skillsRoot, spec.agentsRoot);
1983
1986
  },
1984
1987
  contextIndex(context) {
1988
+ if (spec.name === "codebuddy") {
1989
+ return {
1990
+ instructions: spec.instructions,
1991
+ skills_root: spec.skillsRoot,
1992
+ rules: codebuddyRulePaths(context)
1993
+ };
1994
+ }
1985
1995
  const rules = [];
1986
1996
  if (spec.rulesRoot !== null && spec.ruleExt !== null) {
1987
1997
  rules.push(`${spec.rulesRoot}/harness-general${spec.ruleExt}`);
@@ -2001,6 +2011,9 @@ function makeAdapter(spec) {
2001
2011
  boundaries.push(spec.agentsRoot);
2002
2012
  if (spec.rulesRoot !== null)
2003
2013
  boundaries.push(spec.rulesRoot);
2014
+ if (spec.name === "codebuddy") {
2015
+ boundaries.push(".codebuddy/.rules", ".codebuddy/rules");
2016
+ }
2004
2017
  const top = spec.skillsRoot.split("/")[0];
2005
2018
  if (top !== void 0)
2006
2019
  boundaries.push(top);
@@ -2053,6 +2066,17 @@ var ADAPTERS = {
2053
2066
  function getAdapter(name) {
2054
2067
  return ADAPTERS[name];
2055
2068
  }
2069
+ function codebuddyRulePaths(context) {
2070
+ const names = context.profile === "java" ? ["harness-general", "harness-profile-java"] : ["harness-general"];
2071
+ const paths = [];
2072
+ if (context.codebuddySurface !== "cli") {
2073
+ paths.push(...names.map((name) => `.codebuddy/.rules/${name}.mdc`));
2074
+ }
2075
+ if (context.codebuddySurface !== "ide") {
2076
+ paths.push(...names.map((name) => `.codebuddy/rules/${name}.md`));
2077
+ }
2078
+ return paths.sort((left, right) => left.localeCompare(right));
2079
+ }
2056
2080
  function getAdapters(names) {
2057
2081
  return HARNESS_AGENT_ORDER.filter((n) => names.includes(n)).map(getAdapter);
2058
2082
  }
@@ -2068,6 +2092,13 @@ function managedTargetsFor(adapter, bundle, context) {
2068
2092
  if (context.profile === "java") {
2069
2093
  records.push(ruleTarget("rules/harness-profile-java.mdc", ".cursor/rules/harness-profile-java.mdc", CURSOR_JAVA_RULES_CONTENT));
2070
2094
  }
2095
+ } else if (adapter.name === "codebuddy") {
2096
+ for (const targetPath of codebuddyRulePaths(context)) {
2097
+ const isMdc = targetPath.endsWith(".mdc");
2098
+ const isJava = targetPath.includes("harness-profile-java");
2099
+ const content = isJava ? isMdc ? CURSOR_JAVA_RULES_CONTENT : HARNESS_JAVA_RULES_CONTENT : isMdc ? CURSOR_GENERAL_RULES_CONTENT : HARNESS_GENERAL_RULES_CONTENT;
2100
+ records.push(ruleTarget(`rules/${isJava ? "harness-profile-java" : "harness-general"}${isMdc ? ".mdc" : ".md"}`, targetPath, content));
2101
+ }
2071
2102
  }
2072
2103
  assertNoCaseCollisions(records.map((record) => record.target_path));
2073
2104
  return records.sort((left, right) => left.target_path.localeCompare(right.target_path));
@@ -3704,6 +3735,10 @@ function assertPreviewAllowed(preview, skip) {
3704
3735
  }
3705
3736
  }
3706
3737
  var CREDENTIALS_HINT = "\u53EF\u5728\u4EA4\u4E92\u6A21\u5F0F\u4E0B\u5F55\u5165\uFF0C\u6216\u5199\u5165 .harness/credentials.local.yaml\uFF08\u52FF\u63D0\u4EA4 git\uFF09";
3738
+ var STALE_BASELINE_MESSAGE = "\u670D\u52A1\u7AEF\u5DF2\u6709\u66F4\u65B0\u7684\u63A8\u9001\uFF0C\u8BF7\u5148 git pull \u540E\u6267\u884C npx hunter-harness update \u518D\u63A8";
3739
+ function staleBaselineError(code) {
3740
+ return new PushWorkflowError(STALE_BASELINE_MESSAGE, 5, code);
3741
+ }
3707
3742
  function makePreview(baseline, files, confirmedProjectLocal, ignorePaths, deletedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3708
3743
  const filteredFiles = {};
3709
3744
  for (const [path, content] of Object.entries(files)) {
@@ -3748,14 +3783,12 @@ async function pushProject(options) {
3748
3783
  const profile = parseHarnessProfile(project.project.profiles[0]);
3749
3784
  const installedPaths = profile === null ? /* @__PURE__ */ new Set() : new Set(await Promise.all(enabledHarnessAgents(project).map((agent) => managedBundleTargets(options.resourcesRoot, profile, agent))).then((targets) => targets.flatMap((target) => [...target])));
3750
3785
  let preview = makePreview(baseline, await managedFiles(root, project), options.confirmedProjectLocal ?? [], installedPaths);
3751
- const initialSkip = await resolveSensitiveScanSkip(preview, options);
3752
- if (initialSkip.cancelled === true) {
3753
- return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
3754
- }
3755
- let sensitiveScanSkip = initialSkip.skip;
3756
- let sensitiveScanSkipReason = initialSkip.reason;
3757
- assertPreviewAllowed(preview, sensitiveScanSkip);
3758
3786
  if (options.dryRun) {
3787
+ const drySkip = await resolveSensitiveScanSkip(preview, options);
3788
+ if (drySkip.cancelled === true) {
3789
+ return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
3790
+ }
3791
+ assertPreviewAllowed(preview, drySkip.skip);
3759
3792
  return { preview, proposalId: null, projectId: project.project.project_id };
3760
3793
  }
3761
3794
  const localCredentials = await readLocalCredentials(root);
@@ -3789,6 +3822,25 @@ async function pushProject(options) {
3789
3822
  if (parsedServerUrl.protocol !== "https:") {
3790
3823
  throw new PushWorkflowError("server_url must use HTTPS", 3, "SERVER_URL_INVALID");
3791
3824
  }
3825
+ const client = new HunterHarnessApiClient({
3826
+ serverUrl: parsedServerUrl.toString(),
3827
+ token,
3828
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch }
3829
+ });
3830
+ const boundAtStart = project.project.project_id;
3831
+ if (boundAtStart !== null) {
3832
+ const remote = await client.getProject(boundAtStart, uuidV7());
3833
+ if (remote.latest_project_version !== baseline.complete_project_version) {
3834
+ throw staleBaselineError("PROJECT_VERSION_CONFLICT");
3835
+ }
3836
+ }
3837
+ const initialSkip = await resolveSensitiveScanSkip(preview, options);
3838
+ if (initialSkip.cancelled === true) {
3839
+ return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
3840
+ }
3841
+ let sensitiveScanSkip = initialSkip.skip;
3842
+ let sensitiveScanSkipReason = initialSkip.reason;
3843
+ assertPreviewAllowed(preview, sensitiveScanSkip);
3792
3844
  if (options.confirmProposal !== void 0 && !await options.confirmProposal(preview)) {
3793
3845
  return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
3794
3846
  }
@@ -3803,11 +3855,6 @@ async function pushProject(options) {
3803
3855
  await atomicWriteJson(workflowPath, workflow);
3804
3856
  preview = makePreview(baseline, await managedFiles(root, project), options.confirmedProjectLocal ?? [], installedPaths, workflow.created_at);
3805
3857
  const requestId = workflow.request_id;
3806
- const client = new HunterHarnessApiClient({
3807
- serverUrl: parsedServerUrl.toString(),
3808
- token,
3809
- ...options.fetch === void 0 ? {} : { fetch: options.fetch }
3810
- });
3811
3858
  if (project.project.project_id === null) {
3812
3859
  const resolved = await client.resolveProject({
3813
3860
  schema_version: 1,
@@ -3914,8 +3961,8 @@ async function pushProject(options) {
3914
3961
  throw error;
3915
3962
  }
3916
3963
  if (error instanceof ApiError) {
3917
- if (error.code === "STALE_PUSH") {
3918
- throw new PushWorkflowError("\u670D\u52A1\u7AEF\u5DF2\u6709\u66F4\u65B0\u7684\u63A8\u9001\uFF0C\u8BF7\u5148 git pull \u540E\u6267\u884C npx hunter-harness update \u518D\u63A8", 5, "STALE_PUSH");
3964
+ if (error.code === "STALE_PUSH" || error.code === "PROJECT_VERSION_CONFLICT") {
3965
+ throw staleBaselineError(error.code);
3919
3966
  }
3920
3967
  if (error.code === "SENSITIVE_CONTENT_BLOCKED") {
3921
3968
  const details = error.details;
@@ -4107,8 +4154,8 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
4107
4154
  const after = JSON.parse(await readFile11(join12(transactionRoot, "after", "manifest.json"), "utf8"));
4108
4155
  for (const entry of after) {
4109
4156
  const target = join12(projectRoot, entry.path);
4110
- const exists5 = await pathExists(target);
4111
- if (exists5 !== entry.exists || exists5 && await sha256File(target) !== entry.hash) {
4157
+ const exists6 = await pathExists(target);
4158
+ if (exists6 !== entry.exists || exists6 && await sha256File(target) !== entry.hash) {
4112
4159
  throw new Error("cannot rollback dirty path: " + entry.path);
4113
4160
  }
4114
4161
  }
@@ -4120,15 +4167,15 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
4120
4167
  }
4121
4168
  seen.add(snapshot.path);
4122
4169
  const target = join12(projectRoot, snapshot.path);
4123
- const exists5 = await pathExists(target);
4170
+ const exists6 = await pathExists(target);
4124
4171
  if (snapshot.existed && snapshot.snapshot_name !== null) {
4125
4172
  const content = await readFile11(join12(transactionRoot, "before", snapshot.snapshot_name));
4126
4173
  operations.push({
4127
- operation: exists5 ? "modify" : "add",
4174
+ operation: exists6 ? "modify" : "add",
4128
4175
  path: snapshot.path,
4129
4176
  content
4130
4177
  });
4131
- } else if (exists5) {
4178
+ } else if (exists6) {
4132
4179
  operations.push({ operation: "delete", path: snapshot.path });
4133
4180
  }
4134
4181
  }
@@ -4286,17 +4333,20 @@ async function updateProject(options) {
4286
4333
  if (project.project.project_id === null) {
4287
4334
  throw new UpdateWorkflowError("project is not bound to a server", 3, "PROJECT_NOT_BOUND");
4288
4335
  }
4289
- const serverUrl = options.serverUrl ?? project.server.url;
4290
4336
  const tokenEnv = options.tokenEnv ?? project.server.token_env;
4291
- if (serverUrl === null || serverUrl === void 0) {
4292
- throw new UpdateWorkflowError("server_url is required", 3, "SERVER_URL_REQUIRED");
4293
- }
4294
4337
  if (!/^[A-Z_][A-Z0-9_]*$/.test(tokenEnv)) {
4295
4338
  throw new UpdateWorkflowError("token_env is invalid", 3, "TOKEN_ENV_INVALID");
4296
4339
  }
4297
- const token = options.env[tokenEnv];
4298
- if (token === void 0 || token.trim() === "") {
4299
- throw new UpdateWorkflowError("API token environment variable is unset", 8, "TOKEN_INVALID");
4340
+ const local = await readLocalCredentials(root);
4341
+ const serverUrl = options.serverUrl ?? local?.server_url ?? project.server.url;
4342
+ if (serverUrl === null || serverUrl === void 0) {
4343
+ throw new UpdateWorkflowError("server_url is required", 3, "SERVER_URL_REQUIRED");
4344
+ }
4345
+ const envToken = options.env[tokenEnv]?.trim();
4346
+ const localToken = local?.token?.trim();
4347
+ const token = envToken !== void 0 && envToken !== "" ? envToken : localToken !== void 0 && localToken !== "" ? localToken : void 0;
4348
+ if (token === void 0) {
4349
+ throw new UpdateWorkflowError(`API token \u672A\u914D\u7F6E\uFF1A\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF ${tokenEnv}\uFF0C\u6216\u5728 .harness/credentials.local.yaml \u914D\u7F6E token\uFF08\u53EF\u901A\u8FC7 npx hunter-harness push \u5F15\u5BFC\u5199\u5165\uFF09`, 8, "TOKEN_INVALID");
4300
4350
  }
4301
4351
  const requestId = uuidV7();
4302
4352
  const baseline = await readBaseline(root);
@@ -4541,13 +4591,15 @@ function normalizeProfile(value) {
4541
4591
  if (value === "2" || value === "java") return "java";
4542
4592
  throw new InitConfigurationError("\u914D\u7F6E\u7C7B\u578B\u5FC5\u987B\u4E3A general \u6216 java");
4543
4593
  }
4594
+ var ALL_AGENT_TOKENS = /* @__PURE__ */ new Set(["all", "5"]);
4544
4595
  function parseAgentsInput(raw) {
4545
4596
  const trimmed = raw.trim();
4546
4597
  if (trimmed === "") return ["claude-code"];
4547
- if (trimmed === "all") return [...HARNESS_AGENT_ORDER];
4598
+ if (ALL_AGENT_TOKENS.has(trimmed)) return [...HARNESS_AGENT_ORDER];
4548
4599
  const agents = [];
4549
4600
  for (const token of trimmed.split(",")) {
4550
4601
  const value = token.trim();
4602
+ if (ALL_AGENT_TOKENS.has(value)) return [...HARNESS_AGENT_ORDER];
4551
4603
  const byIndex = AGENT_BY_INDEX[value];
4552
4604
  if (byIndex !== void 0) {
4553
4605
  agents.push(byIndex);
@@ -4672,14 +4724,118 @@ function serializeCliResult(result) {
4672
4724
  return JSON.stringify(result) + "\n";
4673
4725
  }
4674
4726
 
4727
+ // src/config/codebuddy-setup.ts
4728
+ import { mkdir as mkdir4, readFile as readFile14, readdir as readdir7, stat as stat4, writeFile as writeFile5 } from "node:fs/promises";
4729
+ import { basename as basename2, dirname as dirname4, extname, join as join15 } from "node:path";
4730
+ var MANAGED_RULE_NAMES = /* @__PURE__ */ new Set([
4731
+ "harness-general.md",
4732
+ "harness-general.mdc",
4733
+ "harness-profile-java.md",
4734
+ "harness-profile-java.mdc"
4735
+ ]);
4736
+ var SENSITIVE_ASSIGNMENT = /(?:password|passwd|token|secret|access[_-]?key|private[_-]?key)\s*[:=]\s*[^\s#]+/i;
4737
+ var PRIVATE_KEY_BLOCK = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i;
4738
+ async function exists4(path) {
4739
+ try {
4740
+ await stat4(path);
4741
+ return true;
4742
+ } catch {
4743
+ return false;
4744
+ }
4745
+ }
4746
+ async function readJsonObject(path) {
4747
+ try {
4748
+ const parsed = JSON.parse(await readFile14(path, "utf8"));
4749
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
4750
+ } catch (error) {
4751
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
4752
+ return null;
4753
+ }
4754
+ }
4755
+ async function inspectCodeBuddySetup(projectRoot) {
4756
+ const rulesRoot = join15(projectRoot, ".claude", "rules");
4757
+ let claudeRules = [];
4758
+ try {
4759
+ claudeRules = (await readdir7(rulesRoot, { withFileTypes: true })).filter((entry) => entry.isFile() && [".md", ".mdc"].includes(extname(entry.name).toLowerCase())).map((entry) => entry.name).filter((name) => !MANAGED_RULE_NAMES.has(name)).sort();
4760
+ } catch (error) {
4761
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
4762
+ }
4763
+ const mcp = await readJsonObject(join15(projectRoot, ".mcp.json"));
4764
+ const servers = mcp?.mcpServers;
4765
+ const configured = servers !== null && typeof servers === "object" && !Array.isArray(servers) && Object.prototype.hasOwnProperty.call(servers, "codegraph");
4766
+ return {
4767
+ claudeRules,
4768
+ hasCodeGraphIndex: await exists4(join15(projectRoot, ".codegraph")),
4769
+ codeGraphConfigured: configured
4770
+ };
4771
+ }
4772
+ function destinationTargets(root, surface, name) {
4773
+ const stem = basename2(name, extname(name));
4774
+ const targets = [];
4775
+ if (surface !== "cli") targets.push(join15(root, ".codebuddy", ".rules", `${stem}.mdc`));
4776
+ if (surface !== "ide") targets.push(join15(root, ".codebuddy", "rules", `${stem}.md`));
4777
+ return targets;
4778
+ }
4779
+ async function applyCodeBuddySetup(options) {
4780
+ const result = {
4781
+ copied: [],
4782
+ preserved: [],
4783
+ skippedSensitive: [],
4784
+ mcpUpdated: false,
4785
+ warnings: []
4786
+ };
4787
+ if (options.syncClaudeRules) {
4788
+ const plan = await inspectCodeBuddySetup(options.projectRoot);
4789
+ for (const name of plan.claudeRules) {
4790
+ const source = join15(options.projectRoot, ".claude", "rules", name);
4791
+ const content = await readFile14(source, "utf8");
4792
+ if (SENSITIVE_ASSIGNMENT.test(content) || PRIVATE_KEY_BLOCK.test(content)) {
4793
+ result.skippedSensitive.push(name);
4794
+ continue;
4795
+ }
4796
+ for (const target of destinationTargets(options.projectRoot, options.surface, name)) {
4797
+ if (await exists4(target)) {
4798
+ result.preserved.push(target);
4799
+ continue;
4800
+ }
4801
+ await mkdir4(dirname4(target), { recursive: true });
4802
+ await writeFile5(target, content, { encoding: "utf8", flag: "wx" });
4803
+ result.copied.push(target);
4804
+ }
4805
+ }
4806
+ }
4807
+ if (options.configureCodeGraph) {
4808
+ const path = join15(options.projectRoot, ".mcp.json");
4809
+ const current = await readJsonObject(path);
4810
+ if (current === null) {
4811
+ result.warnings.push(".mcp.json \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u5DF2\u4FDD\u7559\u539F\u6587\u4EF6\u5E76\u8DF3\u8FC7 CodeGraph MCP \u914D\u7F6E");
4812
+ } else {
4813
+ const currentServers = current.mcpServers;
4814
+ const servers = currentServers !== null && typeof currentServers === "object" && !Array.isArray(currentServers) ? currentServers : {};
4815
+ if (!Object.prototype.hasOwnProperty.call(servers, "codegraph")) {
4816
+ const next = {
4817
+ ...current,
4818
+ mcpServers: {
4819
+ ...servers,
4820
+ codegraph: { command: "codegraph", args: ["serve", "--mcp"] }
4821
+ }
4822
+ };
4823
+ await writeFile5(path, JSON.stringify(next, null, 2) + "\n", "utf8");
4824
+ result.mcpUpdated = true;
4825
+ }
4826
+ }
4827
+ }
4828
+ return result;
4829
+ }
4830
+
4675
4831
  // src/commands/refresh.ts
4676
- import { readFile as readFile14 } from "node:fs/promises";
4677
- import { join as join15 } from "node:path";
4832
+ import { readFile as readFile15 } from "node:fs/promises";
4833
+ import { join as join16 } from "node:path";
4678
4834
  import { parse as parseYaml9 } from "yaml";
4679
4835
  async function detectProject(root) {
4680
4836
  let content;
4681
4837
  try {
4682
- content = await readFile14(join15(root, ".harness", "project.yaml"), "utf8");
4838
+ content = await readFile15(join16(root, ".harness", "project.yaml"), "utf8");
4683
4839
  } catch (error) {
4684
4840
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
4685
4841
  return { status: "absent" };
@@ -4707,8 +4863,14 @@ function refreshAgents(config) {
4707
4863
  }));
4708
4864
  return agents.length > 0 ? agents : ["claude-code"];
4709
4865
  }
4710
- function codebuddySurface(config) {
4711
- return config.adapter_options?.codebuddy?.surface ?? "both";
4866
+ function codebuddySurface(config, override) {
4867
+ const value = override ?? config.adapter_options?.codebuddy?.surface ?? "both";
4868
+ if (value === "both" || value === "ide" || value === "cli") return value;
4869
+ throw new InitConfigurationError(
4870
+ "codebuddy surface \u5FC5\u987B\u4E3A both\u3001ide \u6216 cli",
4871
+ 3,
4872
+ "CODEBUDDY_SURFACE_INVALID"
4873
+ );
4712
4874
  }
4713
4875
  function summarize(result) {
4714
4876
  const items = [
@@ -4795,7 +4957,7 @@ async function runRefresh(options, dependencies) {
4795
4957
  resourcesRoot: dependencies.resourcesRoot,
4796
4958
  ...targetProfile === void 0 ? {} : { profile: targetProfile },
4797
4959
  agents: targetAgents,
4798
- codebuddySurface: codebuddySurface(detection.config),
4960
+ codebuddySurface: codebuddySurface(detection.config, options.codebuddySurface),
4799
4961
  dryRun: true,
4800
4962
  forceManaged: options.forceManaged === true
4801
4963
  });
@@ -4831,7 +4993,7 @@ async function runRefresh(options, dependencies) {
4831
4993
  resourcesRoot: dependencies.resourcesRoot,
4832
4994
  ...targetProfile === void 0 ? {} : { profile: targetProfile },
4833
4995
  agents: targetAgents,
4834
- codebuddySurface: codebuddySurface(detection.config),
4996
+ codebuddySurface: codebuddySurface(detection.config, options.codebuddySurface),
4835
4997
  dryRun,
4836
4998
  forceManaged: options.forceManaged === true
4837
4999
  });
@@ -4880,6 +5042,54 @@ var AGENT_LABELS = {
4880
5042
  cursor: "Cursor",
4881
5043
  codebuddy: "CodeBuddy"
4882
5044
  };
5045
+ function agentMenuLines(installedProfiles) {
5046
+ const lines = HARNESS_AGENT_ORDER.map((agent, index) => {
5047
+ const profile = installedProfiles?.[agent];
5048
+ const suffix = profile === void 0 ? "" : `\uFF08\u5DF2\u5B89\u88C5\uFF1A${profile}\uFF09`;
5049
+ return ` ${index + 1}. ${AGENT_LABELS[agent]}${suffix}`;
5050
+ }).join("\n");
5051
+ return lines + "\n 5. \u5168\u90E8";
5052
+ }
5053
+ async function configureCodeBuddyExtras(agents, surface, options, dependencies) {
5054
+ if (!agents.includes("codebuddy")) return;
5055
+ const plan = await inspectCodeBuddySetup(dependencies.cwd);
5056
+ let syncClaudeRules = false;
5057
+ let configureCodeGraph = false;
5058
+ if (plan.claudeRules.length > 0) {
5059
+ syncClaudeRules = options.nonInteractive === true ? options.yes === true : /^(?:|y|yes)$/i.test((await dependencies.prompt(
5060
+ `\u53D1\u73B0 ${plan.claudeRules.length} \u4E2A Claude \u81EA\u5B9A\u4E49\u89C4\u5219\uFF0C\u662F\u5426\u590D\u5236\u5230 CodeBuddy\uFF08\u4FDD\u7559\u6E90\u6587\u4EF6\u4E14\u4E0D\u8986\u76D6\u76EE\u6807\uFF09\uFF1F[Y/n]\uFF1A`
5061
+ )).trim());
5062
+ }
5063
+ if (plan.hasCodeGraphIndex && !plan.codeGraphConfigured) {
5064
+ configureCodeGraph = options.nonInteractive === true ? options.yes === true : /^(?:|y|yes)$/i.test((await dependencies.prompt(
5065
+ "\u68C0\u6D4B\u5230 .codegraph \u7D22\u5F15\uFF0C\u662F\u5426\u5408\u5E76 CodeGraph MCP \u5230\u9879\u76EE .mcp.json\uFF1F[Y/n]\uFF1A"
5066
+ )).trim());
5067
+ }
5068
+ if (options.dryRun === true) {
5069
+ if (syncClaudeRules || configureCodeGraph) {
5070
+ dependencies.stdout("CodeBuddy \u9644\u52A0\u914D\u7F6E\u5904\u4E8E dry-run\uFF0C\u672A\u5199\u5165\u89C4\u5219\u6216 .mcp.json\u3002\n");
5071
+ }
5072
+ return;
5073
+ }
5074
+ const result = await applyCodeBuddySetup({
5075
+ projectRoot: dependencies.cwd,
5076
+ surface,
5077
+ syncClaudeRules,
5078
+ configureCodeGraph
5079
+ });
5080
+ if (result.copied.length > 0) {
5081
+ dependencies.stdout(`\u5DF2\u5B89\u5168\u590D\u5236 ${result.copied.length} \u4E2A CodeBuddy \u89C4\u5219\u6587\u4EF6\u3002
5082
+ `);
5083
+ }
5084
+ if (result.skippedSensitive.length > 0) {
5085
+ dependencies.stderr(
5086
+ `\u68C0\u6D4B\u5230 ${result.skippedSensitive.length} \u4E2A\u7591\u4F3C\u542B\u51ED\u636E\u7684 Claude \u89C4\u5219\uFF0C\u5DF2\u4FDD\u7559\u539F\u5904\u4E14\u672A\u590D\u5236\u3002
5087
+ `
5088
+ );
5089
+ }
5090
+ if (result.mcpUpdated) dependencies.stdout("\u5DF2\u5408\u5E76 CodeGraph MCP \u5230 .mcp.json\u3002\n");
5091
+ for (const warning of result.warnings) dependencies.stderr(warning + "\n");
5092
+ }
4883
5093
  async function runFirstInstall(options, dependencies) {
4884
5094
  const requestId = uuidV7();
4885
5095
  try {
@@ -4889,7 +5099,7 @@ async function runFirstInstall(options, dependencies) {
4889
5099
  options,
4890
5100
  options.nonInteractive === true ? {} : {
4891
5101
  agents: () => dependencies.prompt(
4892
- "\u8BF7\u9009\u62E9\u76EE\u6807 Agent\uFF08\u53EF\u591A\u9009\uFF0C\u4F7F\u7528\u9017\u53F7\u5206\u9694\uFF09\n 1. Claude Code\n 2. Codex\n 3. Cursor\n 4. CodeBuddy\n\u8BF7\u8F93\u5165\u7F16\u53F7 [1]: "
5102
+ "\u8BF7\u9009\u62E9\u76EE\u6807 Agent\uFF08\u53EF\u591A\u9009\uFF0C\u4F7F\u7528\u9017\u53F7\u5206\u9694\uFF09\n" + agentMenuLines() + "\n\u8BF7\u8F93\u5165\u7F16\u53F7 [1]: "
4893
5103
  ).then((answer) => answer.trim()),
4894
5104
  profile: () => dependencies.prompt(
4895
5105
  "\u8BF7\u9009\u62E9 Harness \u7C7B\u578B\uFF1A\n1. \u901A\u7528\uFF08\u9ED8\u8BA4\uFF09\n2. Java\n\u8BF7\u8F93\u5165 1 \u6216 2 [1]: "
@@ -4910,6 +5120,12 @@ async function runFirstInstall(options, dependencies) {
4910
5120
  config,
4911
5121
  dryRun: options.dryRun === true
4912
5122
  });
5123
+ await configureCodeBuddyExtras(
5124
+ config.agents,
5125
+ config.codebuddy_surface,
5126
+ options,
5127
+ dependencies
5128
+ );
4913
5129
  const output = {
4914
5130
  schema_version: 1,
4915
5131
  command: "configure",
@@ -4949,20 +5165,29 @@ async function runFirstInstall(options, dependencies) {
4949
5165
  return exitCode;
4950
5166
  }
4951
5167
  }
4952
- async function runExistingProject(options, dependencies, currentProfile) {
5168
+ async function runExistingProject(options, dependencies, currentProfile, currentSurface) {
4953
5169
  const refreshOptions = {};
4954
5170
  if (options.agents !== void 0) refreshOptions.agents = options.agents;
5171
+ if (options.codebuddySurface !== void 0) {
5172
+ refreshOptions.codebuddySurface = options.codebuddySurface;
5173
+ }
4955
5174
  if (options.profile !== void 0) refreshOptions.profile = options.profile;
4956
5175
  if (options.nonInteractive !== void 0) refreshOptions.nonInteractive = options.nonInteractive;
4957
5176
  if (options.yes !== void 0) refreshOptions.yes = options.yes;
4958
5177
  if (options.dryRun !== void 0) refreshOptions.dryRun = options.dryRun;
4959
5178
  if (options.json !== void 0) refreshOptions.json = options.json;
4960
5179
  if (options.forceManaged !== void 0) refreshOptions.forceManaged = options.forceManaged;
4961
- if (options.nonInteractive === true) {
4962
- return runRefresh(refreshOptions, dependencies);
4963
- }
4964
5180
  const installed = await readInstalledAgentConfiguration(dependencies.cwd);
4965
5181
  const currentAgents = installed.agents.length > 0 ? installed.agents : ["claude-code"];
5182
+ if (options.nonInteractive === true) {
5183
+ const selectedAgents2 = options.agents === void 0 ? currentAgents : parseAgentsInput(options.agents);
5184
+ const code2 = await runRefresh(refreshOptions, dependencies);
5185
+ if (code2 === 0) {
5186
+ const surface = options.codebuddySurface === "ide" || options.codebuddySurface === "cli" || options.codebuddySurface === "both" ? options.codebuddySurface : currentSurface;
5187
+ await configureCodeBuddyExtras(selectedAgents2, surface, options, dependencies);
5188
+ }
5189
+ return code2;
5190
+ }
4966
5191
  const currentLines = currentAgents.map(
4967
5192
  (agent) => `- ${AGENT_LABELS[agent]}\uFF1A${installed.profiles[agent] ?? currentProfile}`
4968
5193
  ).join("\n");
@@ -4972,10 +5197,7 @@ async function runExistingProject(options, dependencies, currentProfile) {
4972
5197
  `Hunter Harness \u5F53\u524D\u914D\u7F6E\uFF1A
4973
5198
  ${currentLines}
4974
5199
  \u8BF7\u9009\u62E9\u672C\u6B21\u8981\u65B0\u589E\u6216\u5237\u65B0\u7684\u5DE5\u5177\uFF08\u53EF\u591A\u9009\uFF0C\u9017\u53F7\u5206\u9694\uFF1B\u672A\u9009\u62E9\u7684\u5DE5\u5177\u4FDD\u6301\u4E0D\u53D8\uFF09\uFF1A
4975
- 1. Claude Code
4976
- 2. Codex
4977
- 3. Cursor
4978
- 4. CodeBuddy
5200
+ ` + agentMenuLines(installed.profiles) + `
4979
5201
  \u8BF7\u8F93\u5165\u7F16\u53F7 [${defaultSelection}]\uFF0C\u6216\u8F93\u5165 0 \u53D6\u6D88\uFF1A`
4980
5202
  );
4981
5203
  if (answer.trim() === "0" || /^c/i.test(answer.trim())) return 2;
@@ -4997,7 +5219,13 @@ ${currentLines}
4997
5219
  refreshOptions.profile = answer.trim() === "" ? defaultProfile : answer.trim();
4998
5220
  }
4999
5221
  refreshOptions.confirmed = true;
5000
- return runRefresh(refreshOptions, dependencies);
5222
+ const selectedAgents = parseAgentsInput(refreshOptions.agents);
5223
+ const code = await runRefresh(refreshOptions, dependencies);
5224
+ if (code === 0) {
5225
+ const surface = options.codebuddySurface === "ide" || options.codebuddySurface === "cli" || options.codebuddySurface === "both" ? options.codebuddySurface : currentSurface;
5226
+ await configureCodeBuddyExtras(selectedAgents, surface, options, dependencies);
5227
+ }
5228
+ return code;
5001
5229
  }
5002
5230
  async function runConfigure(options, dependencies) {
5003
5231
  const detection = await detectProject(dependencies.cwd);
@@ -5022,7 +5250,8 @@ async function runConfigure(options, dependencies) {
5022
5250
  }
5023
5251
  if (detection.status === "valid") {
5024
5252
  const currentProfile = detection.config.project.profiles[0] ?? "general";
5025
- return runExistingProject(options, dependencies, currentProfile);
5253
+ const currentSurface = detection.config.adapter_options?.codebuddy?.surface ?? "both";
5254
+ return runExistingProject(options, dependencies, currentProfile, currentSurface);
5026
5255
  }
5027
5256
  return runFirstInstall(options, dependencies);
5028
5257
  }
@@ -5371,11 +5600,11 @@ async function runUpdate(options, dependencies) {
5371
5600
  }
5372
5601
 
5373
5602
  // src/commands/recovery.ts
5374
- import { stat as stat4 } from "node:fs/promises";
5375
- import { join as join16 } from "node:path";
5376
- async function exists4(path) {
5603
+ import { stat as stat5 } from "node:fs/promises";
5604
+ import { join as join17 } from "node:path";
5605
+ async function exists5(path) {
5377
5606
  try {
5378
- await stat4(path);
5607
+ await stat5(path);
5379
5608
  return true;
5380
5609
  } catch {
5381
5610
  return false;
@@ -5432,7 +5661,7 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
5432
5661
  return 5;
5433
5662
  }
5434
5663
  }
5435
- const initialized = await exists4(join16(dependencies.cwd, ".harness", "project.yaml"));
5664
+ const initialized = await exists5(join17(dependencies.cwd, ".harness", "project.yaml"));
5436
5665
  if (!initialized || options.nonInteractive === true || explicitConfigure(options)) {
5437
5666
  return null;
5438
5667
  }
@@ -5475,8 +5704,8 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
5475
5704
  }
5476
5705
 
5477
5706
  // src/workflow-data/resolve.ts
5478
- import { cp, mkdir as mkdir4, readdir as readdir7, readFile as readFile15, rm as rm8, stat as stat5 } from "node:fs/promises";
5479
- import { dirname as dirname4, join as join17, relative as relative2 } from "node:path";
5707
+ import { cp, mkdir as mkdir5, readdir as readdir8, readFile as readFile16, rm as rm8, stat as stat6 } from "node:fs/promises";
5708
+ import { dirname as dirname5, join as join18, relative as relative2 } from "node:path";
5480
5709
  import { fileURLToPath } from "node:url";
5481
5710
  var WorkflowDataResolutionError = class extends Error {
5482
5711
  code;
@@ -5490,7 +5719,7 @@ var WorkflowDataResolutionError = class extends Error {
5490
5719
  };
5491
5720
  async function pathExists3(path) {
5492
5721
  try {
5493
- await stat5(path);
5722
+ await stat6(path);
5494
5723
  return true;
5495
5724
  } catch {
5496
5725
  return false;
@@ -5510,17 +5739,17 @@ function workflowPackageName(family, env) {
5510
5739
  return `${scope}/workflow-${family}`;
5511
5740
  }
5512
5741
  async function listFilesRecursive(root, base = root) {
5513
- const entries = await readdir7(root, { withFileTypes: true });
5742
+ const entries = await readdir8(root, { withFileTypes: true });
5514
5743
  const files = [];
5515
5744
  for (const entry of entries) {
5516
- const absolute = join17(root, entry.name);
5745
+ const absolute = join18(root, entry.name);
5517
5746
  if (entry.isDirectory()) {
5518
5747
  files.push(...await listFilesRecursive(absolute, base));
5519
5748
  continue;
5520
5749
  }
5521
5750
  if (!entry.isFile()) continue;
5522
5751
  const rel = relative2(base, absolute).replaceAll("\\", "/");
5523
- files.push({ path: rel, content: await readFile15(absolute, "utf8") });
5752
+ files.push({ path: rel, content: await readFile16(absolute, "utf8") });
5524
5753
  }
5525
5754
  return files;
5526
5755
  }
@@ -5533,7 +5762,7 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
5533
5762
  const manifest = await readWorkflowFamilyManifest(resourcesRoot);
5534
5763
  const expected = manifest.content_sha256;
5535
5764
  if (typeof expected !== "string" || expected.length === 0) return;
5536
- const harnessRoot = join17(resourcesRoot, "harness");
5765
+ const harnessRoot = join18(resourcesRoot, "harness");
5537
5766
  if (!await pathExists3(harnessRoot)) {
5538
5767
  throw new WorkflowDataResolutionError(
5539
5768
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u7F3A\u5C11 harness/\uFF0C\u65E0\u6CD5\u6821\u9A8C SHA-256",
@@ -5552,40 +5781,40 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
5552
5781
  }
5553
5782
  }
5554
5783
  async function monorepoResourcesRoot() {
5555
- const here = dirname4(fileURLToPath(import.meta.url));
5784
+ const here = dirname5(fileURLToPath(import.meta.url));
5556
5785
  const candidates = [
5557
5786
  // TypeScript source: packages/cli/src/workflow-data -> packages/workflow-data-harness.
5558
- join17(here, "../../../workflow-data-harness"),
5787
+ join18(here, "../../../workflow-data-harness"),
5559
5788
  // Bundled CLI: packages/cli/dist -> packages/workflow-data-harness.
5560
- join17(here, "../../workflow-data-harness"),
5789
+ join18(here, "../../workflow-data-harness"),
5561
5790
  // Test/build layouts that preserve additional source directory levels.
5562
- join17(here, "../../../../packages/workflow-data-harness")
5791
+ join18(here, "../../../../packages/workflow-data-harness")
5563
5792
  ];
5564
5793
  for (const candidate of candidates) {
5565
- if (await pathExists3(join17(candidate, "harness", "manifests"))) return candidate;
5794
+ if (await pathExists3(join18(candidate, "harness", "manifests"))) return candidate;
5566
5795
  }
5567
5796
  return null;
5568
5797
  }
5569
5798
  async function siblingWorkflowPackage(cwd) {
5570
- const here = dirname4(fileURLToPath(import.meta.url));
5799
+ const here = dirname5(fileURLToPath(import.meta.url));
5571
5800
  const candidates = [
5572
- join17(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
5801
+ join18(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
5573
5802
  // Published layout: node_modules/hunter-harness/dist/bin.js alongside the
5574
5803
  // scoped workflow package in node_modules/@hunter-harness/workflow-harness.
5575
- join17(here, "..", "..", "@hunter-harness", "workflow-harness"),
5804
+ join18(here, "..", "..", "@hunter-harness", "workflow-harness"),
5576
5805
  // Monorepo bundled layout: packages/cli/dist/bin.js.
5577
- join17(here, "..", "..", "workflow-data-harness")
5806
+ join18(here, "..", "..", "workflow-data-harness")
5578
5807
  ];
5579
5808
  for (const candidate of candidates) {
5580
- if (await pathExists3(join17(candidate, "harness", "manifests"))) return candidate;
5809
+ if (await pathExists3(join18(candidate, "harness", "manifests"))) return candidate;
5581
5810
  }
5582
5811
  return null;
5583
5812
  }
5584
5813
  async function readCachedNpmPackageVersion(cacheRoot) {
5585
- const manifestPath = join17(cacheRoot, "package.json");
5814
+ const manifestPath = join18(cacheRoot, "package.json");
5586
5815
  if (!await pathExists3(manifestPath)) return null;
5587
5816
  try {
5588
- const pkg = JSON.parse(await readFile15(manifestPath, "utf8"));
5817
+ const pkg = JSON.parse(await readFile16(manifestPath, "utf8"));
5589
5818
  return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : null;
5590
5819
  } catch {
5591
5820
  return null;
@@ -5607,13 +5836,13 @@ async function latestWorkflowCacheIsStale(cacheRoot, packageName, resolveManifes
5607
5836
  }
5608
5837
  }
5609
5838
  async function materializeWorkflowPackageCache(cacheRoot, packageSpec, extract) {
5610
- await mkdir4(cacheRoot, { recursive: true });
5611
- const staging = join17(cacheRoot, ".extract");
5839
+ await mkdir5(cacheRoot, { recursive: true });
5840
+ const staging = join18(cacheRoot, ".extract");
5612
5841
  await rm8(staging, { recursive: true, force: true });
5613
- await mkdir4(staging, { recursive: true });
5842
+ await mkdir5(staging, { recursive: true });
5614
5843
  await extract(packageSpec, staging);
5615
- const packageDir = await pathExists3(join17(staging, "harness", "manifests")) ? staging : join17(staging, "package");
5616
- if (!await pathExists3(join17(packageDir, "harness", "manifests"))) {
5844
+ const packageDir = await pathExists3(join18(staging, "harness", "manifests")) ? staging : join18(staging, "package");
5845
+ if (!await pathExists3(join18(packageDir, "harness", "manifests"))) {
5617
5846
  throw new WorkflowDataResolutionError(
5618
5847
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u5185\u5BB9\u65E0\u6548\uFF1A\u7F3A\u5C11 harness/manifests",
5619
5848
  "WORKFLOW_DATA_INVALID",
@@ -5642,8 +5871,8 @@ async function resolveWorkflowResourcesRoot(options, argv = []) {
5642
5871
  const packageName = workflowPackageName(family, options.env);
5643
5872
  const packageSpec = version === "latest" ? packageName : `${packageName}@${version}`;
5644
5873
  const cacheKey = packageSpec.replace("/", "+");
5645
- const cacheRoot = join17(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
5646
- if (await pathExists3(join17(cacheRoot, "harness", "manifests"))) {
5874
+ const cacheRoot = join18(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
5875
+ if (await pathExists3(join18(cacheRoot, "harness", "manifests"))) {
5647
5876
  if (version === "latest") {
5648
5877
  const stale = await latestWorkflowCacheIsStale(cacheRoot, packageName, options.pacoteManifest);
5649
5878
  if (stale) {
@@ -5689,9 +5918,9 @@ function describeWorkflowDataFetchFailure(error, packageSpec) {
5689
5918
  return `\u65E0\u6CD5\u83B7\u53D6\u5DE5\u4F5C\u6D41\u6570\u636E\u5305 ${packageSpec}\uFF1A\u672C\u5730\u7F13\u5B58\u4E0D\u5B58\u5728\u4E14\u83B7\u53D6\u5931\u8D25\u3002\u53EF${hintRoot}\u3002\u539F\u56E0\uFF1A${code !== "" ? code + " " : ""}${detail}`;
5690
5919
  }
5691
5920
  async function readWorkflowFamilyManifest(resourcesRoot) {
5692
- const manifestPath = join17(resourcesRoot, "hunter-workflow-family.json");
5921
+ const manifestPath = join18(resourcesRoot, "hunter-workflow-family.json");
5693
5922
  if (!await pathExists3(manifestPath)) return {};
5694
- return JSON.parse(await readFile15(manifestPath, "utf8"));
5923
+ return JSON.parse(await readFile16(manifestPath, "utf8"));
5695
5924
  }
5696
5925
 
5697
5926
  // src/bin.ts
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
- {
1
+ {
2
2
  "name": "hunter-harness",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Local-first, server-governed agent harness",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "engines": {
8
- "node": ">=24.0.0"
8
+ "node": ">=22.12.0"
9
9
  },
10
10
  "bin": {
11
11
  "hunter-harness": "dist/bin.js"
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && npm run bundle",
18
- "bundle": "node ../../scripts/bundle-cli.mjs",
18
+ "bundle": "node ../../scripts/bundle-cli.mjs",
19
19
  "typecheck": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
20
20
  "prepack": "npm run build"
21
21
  },
@@ -25,7 +25,7 @@
25
25
  "zod": "4.4.3"
26
26
  },
27
27
  "optionalDependencies": {
28
- "pacote": "^21.0.0"
28
+ "pacote": "21.4.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@hunter-harness/contracts": "*",