auditor-lambda 0.3.14 → 0.3.15

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
@@ -30,7 +30,8 @@ npm install -g auditor-lambda
30
30
 
31
31
  That makes `audit-code` available on `PATH`. During package install, the package
32
32
  also writes user-level command/skill assets for hosts we can seed safely, including
33
- the Claude command file and global Codex skill bundle.
33
+ the Claude command file, the global Codex skill bundle, and the global OpenCode
34
+ slash command entry in `~/.config/opencode/opencode.json`.
34
35
 
35
36
  After that, invoke `/audit-code` in a supported host. The prompt self-bootstraps
36
37
  the current repository by running:
@@ -53,7 +54,7 @@ That bootstraps repo-local `/audit-code` surfaces for the hosts we can automate
53
54
 
54
55
  - Codex `AGENTS.md` fallback guidance for the global skill surface
55
56
  - Claude Desktop local MCP bundle artifacts and project template guidance
56
- - OpenCode command, skill, and `opencode.json` surfaces
57
+ - OpenCode `opencode.json` with the `/audit-code` slash command and auditor MCP server
57
58
  - VS Code prompt, custom agent, Copilot instructions, and `.vscode/mcp.json`
58
59
  - Antigravity planning-mode guidance plus the shared repo-local MCP launcher
59
60
 
@@ -533,10 +533,18 @@ function renderCodexAutomationRecipe() {
533
533
  ].join('\n');
534
534
  }
535
535
 
536
- function renderOpenCodeProjectConfig(root) {
536
+ function renderOpenCodeProjectConfig(root, promptBody) {
537
537
  const launcher = replaceBackslashes(toRepoRelativePath(root, join(root, '.audit-code', 'install', MCP_LAUNCHER_FILENAME)));
538
538
  return {
539
539
  $schema: 'https://opencode.ai/config.json',
540
+ command: {
541
+ 'audit-code': {
542
+ template: promptBody.trimStart(),
543
+ description: 'Autonomous local loop code auditing',
544
+ agent: 'auditor',
545
+ subtask: false,
546
+ },
547
+ },
540
548
  mcp: {
541
549
  auditor: {
542
550
  type: 'local',
@@ -599,11 +607,15 @@ function objectValue(value) {
599
607
  : {};
600
608
  }
601
609
 
602
- function buildMergedOpenCodeProjectConfig(existing, root) {
603
- const generated = renderOpenCodeProjectConfig(root);
610
+ function buildMergedOpenCodeProjectConfig(existing, root, promptBody) {
611
+ const generated = renderOpenCodeProjectConfig(root, promptBody);
604
612
  return {
605
613
  ...existing,
606
614
  $schema: existing.$schema ?? generated.$schema,
615
+ command: {
616
+ ...objectValue(existing.command),
617
+ 'audit-code': generated.command['audit-code'],
618
+ },
607
619
  mcp: {
608
620
  ...objectValue(existing.mcp),
609
621
  auditor: generated.mcp.auditor,
@@ -1084,17 +1096,15 @@ const INSTALL_HOST_DEFINITIONS = {
1084
1096
  support_level: 'supported',
1085
1097
  setup_kind: 'command+agent+mcp',
1086
1098
  summary:
1087
- 'Use the generated OpenCode command and project config so `/audit-code` and the local auditor MCP server are available together.',
1088
- primary_path_key: 'opencodeCommandPath',
1099
+ 'Use the generated `opencode.json` so the `/audit-code` slash command and the local auditor MCP server are both available.',
1100
+ primary_path_key: 'opencodeConfigPath',
1089
1101
  supporting_path_keys: [
1090
- 'opencodeConfigPath',
1091
- 'opencodeSkillPath',
1092
1102
  'agentsInstructionsPath',
1093
1103
  'mcpLauncherPath',
1094
1104
  ],
1095
1105
  steps: [
1096
1106
  'Open this repository in OpenCode.',
1097
- 'Let OpenCode load the generated `opencode.json` so the auditor MCP server is available.',
1107
+ 'Let OpenCode load the generated `opencode.json` it registers the `/audit-code` slash command and the auditor MCP server.',
1098
1108
  'Invoke `/audit-code` and keep the audit loop on the auditor MCP tools.',
1099
1109
  ],
1100
1110
  profile: {
@@ -1792,63 +1802,28 @@ async function verifyInstalledBootstrap(argv) {
1792
1802
  break;
1793
1803
  }
1794
1804
  case 'opencode':
1795
- await collectVerifyCheck(checks, 'opencode_command', async () => {
1796
- const content = await readFile(assetPaths.opencodeCommandPath, 'utf8');
1797
- if (!content.includes('agent: auditor')) {
1798
- throw new Error(`OpenCode command file is missing the auditor agent frontmatter: ${assetPaths.opencodeCommandPath}`);
1799
- }
1800
- const { body: commandBody } = splitFrontmatter(content);
1801
- const { body: sourceBody } = splitFrontmatter(await readFile(promptAssetPath, 'utf8'));
1802
- if (commandBody !== sourceBody.trimStart()) {
1803
- throw new Error(
1804
- `OpenCode command prompt body is out of sync with the source prompt. Run "audit-code install --host opencode" or "audit-code install".`,
1805
- );
1806
- }
1807
- return {
1808
- summary: 'OpenCode command file is present and uses the source prompt body.',
1809
- path: assetPaths.opencodeCommandPath,
1810
- };
1811
- });
1812
- await collectVerifyCheck(checks, 'opencode_skill', async () => {
1813
- const content = (await readFile(assetPaths.opencodeSkillPath, 'utf8')).replace(/\r\n/g, '\n');
1814
- const sourceSkill = (await readFile(skillAssetPath, 'utf8')).replace(/\r\n/g, '\n');
1815
- if (content !== sourceSkill) {
1816
- throw new Error(
1817
- `OpenCode skill is out of sync with the source skill. Run "audit-code install --host opencode" or "audit-code install".`,
1818
- );
1819
- }
1820
- return {
1821
- summary: 'OpenCode skill is present and matches the source skill.',
1822
- path: assetPaths.opencodeSkillPath,
1823
- };
1824
- });
1825
- await collectVerifyCheck(checks, 'opencode_prompt', async () => {
1826
- const content = await readFile(assetPaths.opencodePromptPath, 'utf8');
1827
- const sourcePrompt = await readFile(promptAssetPath, 'utf8');
1828
- if (content !== sourcePrompt) {
1829
- throw new Error(
1830
- `OpenCode prompt is out of sync with the source prompt. Run "audit-code install --host opencode" or "audit-code install".`,
1831
- );
1832
- }
1833
- return {
1834
- summary: 'OpenCode prompt is present and matches the source prompt.',
1835
- path: assetPaths.opencodePromptPath,
1836
- };
1837
- });
1838
1805
  await collectVerifyCheck(checks, 'opencode_config', async () => {
1839
1806
  const config = await readJson(assetPaths.opencodeConfigPath, 'OpenCode project config');
1840
- const command = config?.mcp?.auditor?.command;
1841
- if (!Array.isArray(command) || command[0] !== 'node') {
1807
+ const mcpCommand = config?.mcp?.auditor?.command;
1808
+ if (!Array.isArray(mcpCommand) || mcpCommand[0] !== 'node') {
1842
1809
  throw new Error('OpenCode config must set mcp.auditor.command as a Node command array.');
1843
1810
  }
1844
- if (command[1] !== '.audit-code/install/run-mcp-server.mjs') {
1845
- throw new Error(`OpenCode config must point at .audit-code/install/${MCP_LAUNCHER_FILENAME}, got ${command[1] ?? 'missing'}.`);
1811
+ if (mcpCommand[1] !== '.audit-code/install/run-mcp-server.mjs') {
1812
+ throw new Error(`OpenCode config must point at .audit-code/install/${MCP_LAUNCHER_FILENAME}, got ${mcpCommand[1] ?? 'missing'}.`);
1846
1813
  }
1847
1814
  if (config?.mcp?.auditor?.type !== 'local') {
1848
1815
  throw new Error(`OpenCode config must set mcp.auditor.type to "local", got ${config?.mcp?.auditor?.type ?? 'missing'}.`);
1849
1816
  }
1817
+ const commandConfig = config?.command?.['audit-code'];
1818
+ if (!commandConfig?.template) {
1819
+ throw new Error('OpenCode config is missing command["audit-code"].template — the /audit-code slash command will not surface. Run "audit-code install".');
1820
+ }
1821
+ const { body: sourceBody } = splitFrontmatter(await readFile(promptAssetPath, 'utf8'));
1822
+ if (commandConfig.template !== sourceBody.trimStart()) {
1823
+ throw new Error('OpenCode config command["audit-code"].template is out of sync with the source prompt. Run "audit-code install".');
1824
+ }
1850
1825
  return {
1851
- summary: 'OpenCode project config parsed successfully.',
1826
+ summary: 'OpenCode project config has MCP server and /audit-code slash command.',
1852
1827
  path: assetPaths.opencodeConfigPath,
1853
1828
  };
1854
1829
  });
@@ -2022,20 +1997,9 @@ async function detectBootstrapRefreshReason(root, host) {
2022
1997
  break;
2023
1998
  }
2024
1999
  case 'opencode': {
2025
- const opencodeSkill = await readTextIfExists(assetPaths.opencodeSkillPath);
2026
- if (opencodeSkill?.replace(/\r\n/g, '\n') !== sourceSkill) {
2027
- return 'stale_host_asset:opencode:skill';
2028
- }
2029
- const opencodePrompt = await readTextIfExists(assetPaths.opencodePromptPath);
2030
- if (opencodePrompt !== sourcePrompt) {
2031
- return 'stale_host_asset:opencode:prompt';
2032
- }
2033
- const opencodeCommand = await readTextIfExists(assetPaths.opencodeCommandPath);
2034
- if (opencodeCommand === null) {
2035
- return 'missing_host_asset:opencode:command';
2036
- }
2037
- if (splitFrontmatter(opencodeCommand).body !== sourcePromptBody.trimStart()) {
2038
- return 'stale_host_asset:opencode:command';
2000
+ const opencodeConfig = await readJson(assetPaths.opencodeConfigPath, 'OpenCode config').catch(() => null);
2001
+ if (opencodeConfig?.command?.['audit-code']?.template !== sourcePromptBody.trimStart()) {
2002
+ return 'stale_host_asset:opencode:config_command';
2039
2003
  }
2040
2004
  break;
2041
2005
  }
@@ -2164,18 +2128,9 @@ async function installBootstrap(argv, options = {}) {
2164
2128
  claudeDesktopMcpbPath: profile.writeClaudeDesktop
2165
2129
  ? join(root, '.audit-code', 'install', 'claude-desktop', 'auditor-lambda.mcpb')
2166
2130
  : null,
2167
- opencodeCommandPath: profile.writeOpenCode
2168
- ? join(root, '.opencode', 'commands', 'audit-code.md')
2169
- : null,
2170
2131
  opencodeConfigPath: profile.writeOpenCode
2171
2132
  ? join(root, 'opencode.json')
2172
2133
  : null,
2173
- opencodeSkillPath: profile.writeOpenCode
2174
- ? join(root, '.opencode', 'skills', 'audit-code', 'SKILL.md')
2175
- : null,
2176
- opencodePromptPath: profile.writeOpenCode
2177
- ? join(root, '.opencode', 'skills', 'audit-code', 'audit-code.prompt.md')
2178
- : null,
2179
2134
  vscodePromptPath: profile.writeVSCode
2180
2135
  ? join(root, '.github', 'prompts', 'audit-code.prompt.md')
2181
2136
  : null,
@@ -2285,36 +2240,39 @@ async function installBootstrap(argv, options = {}) {
2285
2240
  }
2286
2241
 
2287
2242
  if (profile.writeOpenCode) {
2288
- results.push(
2289
- await writeGeneratedMarkdown(
2290
- assetPaths.opencodeCommandPath,
2291
- renderPromptFile(
2292
- {
2293
- description: 'Autonomous local loop code auditing',
2294
- agent: 'auditor',
2295
- subtask: false,
2296
- },
2297
- promptBody,
2298
- ),
2299
- ),
2243
+ // Clean up legacy command/skill/prompt files that were generated by older installs.
2244
+ // The slash command is now registered via the `command` section in opencode.json.
2245
+ const legacyOpenCodeCommandPath = join(root, '.opencode', 'commands', 'audit-code.md');
2246
+ const legacyOpenCodeCommandContent = renderPromptFile(
2247
+ { description: 'Autonomous local loop code auditing', agent: 'auditor', subtask: false },
2248
+ promptBody,
2300
2249
  );
2301
- results.push(
2302
- await writeGeneratedMarkdown(
2303
- assetPaths.opencodeSkillPath,
2304
- skillSource,
2305
- ),
2250
+ const legacyOpenCodeCommandRemoval = await removeGeneratedMarkdownIfMatches(
2251
+ legacyOpenCodeCommandPath,
2252
+ legacyOpenCodeCommandContent,
2306
2253
  );
2307
- results.push(
2308
- await writeGeneratedMarkdown(
2309
- assetPaths.opencodePromptPath,
2310
- promptSource,
2311
- ),
2254
+ if (legacyOpenCodeCommandRemoval) {
2255
+ results.push(legacyOpenCodeCommandRemoval);
2256
+ }
2257
+ const legacyOpenCodeSkillRemoval = await removeGeneratedMarkdownIfMatches(
2258
+ join(root, '.opencode', 'skills', 'audit-code', 'SKILL.md'),
2259
+ skillSource,
2312
2260
  );
2261
+ if (legacyOpenCodeSkillRemoval) {
2262
+ results.push(legacyOpenCodeSkillRemoval);
2263
+ }
2264
+ const legacyOpenCodePromptRemoval = await removeGeneratedMarkdownIfMatches(
2265
+ join(root, '.opencode', 'skills', 'audit-code', 'audit-code.prompt.md'),
2266
+ promptSource,
2267
+ );
2268
+ if (legacyOpenCodePromptRemoval) {
2269
+ results.push(legacyOpenCodePromptRemoval);
2270
+ }
2313
2271
  results.push(
2314
2272
  await writeMergedGeneratedJson(
2315
2273
  assetPaths.opencodeConfigPath,
2316
2274
  'OpenCode project config',
2317
- (existing) => buildMergedOpenCodeProjectConfig(existing, root),
2275
+ (existing) => buildMergedOpenCodeProjectConfig(existing, root, promptBody),
2318
2276
  ),
2319
2277
  );
2320
2278
  }
@@ -2415,7 +2373,7 @@ async function installBootstrap(argv, options = {}) {
2415
2373
  files: results,
2416
2374
  slash_command_surfaces: {
2417
2375
  vscode_prompt: assetPaths.vscodePromptPath,
2418
- opencode_command: assetPaths.opencodeCommandPath,
2376
+ opencode_config: assetPaths.opencodeConfigPath,
2419
2377
  },
2420
2378
  instruction_surfaces: {
2421
2379
  agents: assetPaths.agentsInstructionsPath,
@@ -43,7 +43,7 @@ Host-specific files may include:
43
43
 
44
44
  - Codex: managed `AGENTS.md` fallback guidance
45
45
  - Claude Desktop: project template, remote MCP connector, local MCP bundle
46
- - OpenCode: command file, skill bundle, and `opencode.json`
46
+ - OpenCode: `opencode.json` with `/audit-code` slash command and auditor MCP server
47
47
  - VS Code/Copilot: prompt, custom agent, instructions, and `.vscode/mcp.json`
48
48
  - Antigravity: planning-mode and MCP-oriented guidance
49
49
 
@@ -62,8 +62,9 @@ repo-local `AGENTS.md` fallback guidance.
62
62
  Claude Desktop is treated as an MCP-first host. Use the generated project
63
63
  template and local bundle artifacts when installing the integration.
64
64
 
65
- OpenCode and VS Code use repo-local prompt, command, and MCP configuration
66
- files generated by `audit-code ensure` or `audit-code install`.
65
+ OpenCode uses `opencode.json` (generated by `audit-code ensure` or `audit-code
66
+ install`) which registers the `/audit-code` slash command and the auditor MCP
67
+ server together. VS Code uses repo-local prompt and MCP configuration files.
67
68
 
68
69
  Antigravity should be treated as a workflow-and-artifacts host until it has a
69
70
  stable project-local config surface. Use generated planning-mode guidance,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",
@@ -25,6 +25,40 @@ function writeGeneratedFile(path, content) {
25
25
  return action;
26
26
  }
27
27
 
28
+ function splitFrontmatter(text) {
29
+ const normalized = text.replace(/\r\n/g, '\n');
30
+ const match = normalized.match(/^---\n([\s\S]*?)\n---\n?/u);
31
+ if (!match) return { body: normalized };
32
+ return { body: normalized.slice(match[0].length) };
33
+ }
34
+
35
+ function mergeOpenCodeGlobalConfig(existing, promptBody) {
36
+ const parsed = existing ? JSON.parse(existing) : {};
37
+ return {
38
+ ...parsed,
39
+ command: {
40
+ ...(parsed.command && typeof parsed.command === 'object' && !Array.isArray(parsed.command)
41
+ ? parsed.command
42
+ : {}),
43
+ 'audit-code': {
44
+ template: promptBody.trimStart(),
45
+ description: 'Autonomous local loop code auditing',
46
+ agent: 'auditor',
47
+ subtask: false,
48
+ },
49
+ },
50
+ };
51
+ }
52
+
53
+ function installMergedJson(path, buildMerged) {
54
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
55
+ const merged = buildMerged(existing);
56
+ const action = existing ? 'updated' : 'installed';
57
+ mkdirSync(dirname(path), { recursive: true });
58
+ writeFileSync(path, JSON.stringify(merged, null, 2) + '\n', 'utf8');
59
+ return action;
60
+ }
61
+
28
62
  const promptSource = readRequiredSource(promptSourceFile, 'prompt');
29
63
  const skillSource = readRequiredSource(skillSourceFile, 'skill');
30
64
 
@@ -32,6 +66,8 @@ if (!promptSource || !skillSource) {
32
66
  process.exit(0);
33
67
  }
34
68
 
69
+ const promptBody = splitFrontmatter(promptSource.toString('utf8')).body;
70
+
35
71
  const installs = [
36
72
  {
37
73
  label: 'Claude command',
@@ -62,3 +98,16 @@ for (const install of installs) {
62
98
  console.warn(` ${install.path}`);
63
99
  }
64
100
  }
101
+
102
+ // Install OpenCode global command via merged config
103
+ const opencodeGlobalConfig = join(homedir(), '.config', 'opencode', 'opencode.json');
104
+ try {
105
+ const action = installMergedJson(opencodeGlobalConfig, (existing) =>
106
+ mergeOpenCodeGlobalConfig(existing, promptBody),
107
+ );
108
+ console.log(`audit-code: ${action} global OpenCode command in ${opencodeGlobalConfig}`);
109
+ } catch (err) {
110
+ console.warn(`audit-code: could not install global OpenCode command (${err.message})`);
111
+ console.warn(` To install manually, add "command": { "audit-code": { "template": "...", "agent": "auditor" } } to:`);
112
+ console.warn(` ${opencodeGlobalConfig}`);
113
+ }