@plainconceptsplatform/agent-harness 3.9.0 → 3.10.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.
Files changed (142) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +551 -551
  3. package/cli/commands/join.js +259 -259
  4. package/cli/commands/migrate.js +310 -310
  5. package/cli/commands/shared.js +27 -27
  6. package/cli/commands/single.js +79 -79
  7. package/cli/commands/update.js +121 -121
  8. package/cli/commands/wizard.js +134 -134
  9. package/cli/fragments/archive/az.md +97 -97
  10. package/cli/fragments/archive/gh.md +96 -96
  11. package/cli/fragments/archive/gl.md +96 -96
  12. package/cli/fragments/archive/none.md +75 -75
  13. package/cli/fragments/guardrails/codegraph.md +5 -5
  14. package/cli/fragments/guardrails/humanizer.md +4 -4
  15. package/cli/fragments/guardrails/memory.md +4 -4
  16. package/cli/fragments/guardrails/rtk.md +3 -3
  17. package/cli/fragments/guardrails/simple-english.md +4 -4
  18. package/cli/fragments/ops-backlog/az.md +28 -28
  19. package/cli/fragments/ops-backlog/gh.md +28 -28
  20. package/cli/fragments/ops-backlog/jira.md +28 -28
  21. package/cli/fragments/ops-review/az.md +62 -62
  22. package/cli/fragments/ops-review/gh.md +51 -51
  23. package/cli/fragments/ops-review/gl.md +55 -55
  24. package/cli/fragments/ops-ship/az.md +81 -81
  25. package/cli/fragments/ops-ship/gh.md +67 -67
  26. package/cli/fragments/ops-ship/gl.md +84 -84
  27. package/cli/index.js +116 -116
  28. package/cli/presets/agents-content.json +34 -34
  29. package/cli/presets/browser.json +10 -10
  30. package/cli/presets/clean.json +21 -21
  31. package/cli/presets/models.json +68 -68
  32. package/cli/presets/optimization.json +32 -32
  33. package/cli/presets/platforms.json +76 -76
  34. package/cli/presets/source.json +23 -23
  35. package/cli/steps/browser/index.js +52 -48
  36. package/cli/steps/clean/index.js +120 -120
  37. package/cli/steps/copy/agents.js +176 -176
  38. package/cli/steps/copy/commands.js +79 -79
  39. package/cli/steps/copy/fullstack-engineer.js +81 -81
  40. package/cli/steps/copy/index.js +88 -88
  41. package/cli/steps/copy/opencode-json.js +359 -324
  42. package/cli/steps/copy/skills.js +316 -316
  43. package/cli/steps/metadata/index.js +115 -115
  44. package/cli/steps/models/format.js +88 -88
  45. package/cli/steps/models/index.js +64 -64
  46. package/cli/steps/models/write.js +38 -38
  47. package/cli/steps/openspec/index.js +136 -136
  48. package/cli/steps/optimization/codegraph.js +141 -141
  49. package/cli/steps/optimization/detect.js +66 -66
  50. package/cli/steps/optimization/humanizer.js +17 -17
  51. package/cli/steps/optimization/index.js +158 -158
  52. package/cli/steps/optimization/memory.js +91 -91
  53. package/cli/steps/optimization/patch-guardrails.js +111 -111
  54. package/cli/steps/optimization/simple-english.js +17 -17
  55. package/cli/steps/optimization/skills-lock.js +30 -30
  56. package/cli/steps/platform/index.js +109 -109
  57. package/cli/steps/source/index.js +123 -123
  58. package/cli/utils/agent-color.js +111 -111
  59. package/cli/utils/copy.js +134 -134
  60. package/cli/utils/exec-spinner.js +47 -47
  61. package/cli/utils/exec.js +134 -134
  62. package/cli/utils/legacy-check.js +30 -30
  63. package/cli/utils/models-cache.js +58 -58
  64. package/cli/utils/models-pricing.js +42 -42
  65. package/cli/utils/paths.js +82 -82
  66. package/cli/utils/process.js +3 -3
  67. package/cli/utils/terminal.js +6 -6
  68. package/cli/utils/update-manifest.js +75 -75
  69. package/harness/.opencode/_gitignore +12 -12
  70. package/harness/.opencode/commands/init.md +5 -5
  71. package/harness/.opencode/commands/make-architecture.md +5 -5
  72. package/harness/.opencode/commands/make-design.md +5 -5
  73. package/harness/.opencode/commands/make-engineer.md +5 -5
  74. package/harness/.opencode/commands/make-guardrails.md +5 -5
  75. package/harness/.opencode/commands/make-user-model.md +5 -5
  76. package/harness/.opencode/commands/ops-backlog.md +10 -10
  77. package/harness/.opencode/commands/ops-review.md +10 -10
  78. package/harness/.opencode/commands/ops-ship.md +9 -9
  79. package/harness/.opencode/commands/plan-apply.md +9 -9
  80. package/harness/.opencode/commands/plan-archive.md +7 -7
  81. package/harness/.opencode/commands/plan-explore.md +11 -11
  82. package/harness/.opencode/commands/plan-goal.md +5 -5
  83. package/harness/.opencode/commands/plan-propose.md +11 -11
  84. package/harness/.opencode/commands/plan-quick.md +7 -7
  85. package/harness/.opencode/commands/plan-story.md +11 -11
  86. package/harness/.opencode/commands/repo-audit.md +7 -7
  87. package/harness/.opencode/commands/repo-help.md +5 -5
  88. package/harness/.opencode/commands/repo-initialize.md +5 -5
  89. package/harness/.opencode/commands/repo-onboard.md +5 -5
  90. package/harness/.opencode/commands/repo-verify.md +5 -5
  91. package/harness/.opencode/plugins/pc-commit-coauthor.js +41 -41
  92. package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
  93. package/harness/.opencode/plugins/pc-system-reminders.js +424 -424
  94. package/harness/.opencode/scripts/check-diff.mjs +305 -305
  95. package/harness/.opencode/scripts/code-rules.mjs +288 -288
  96. package/harness/.opencode/scripts/platform-contract.mjs +366 -366
  97. package/harness/AGENTS.md +55 -55
  98. package/harness/ARCHITECTURE.md +13 -13
  99. package/harness/DESIGN.md +13 -13
  100. package/harness/opencode.jsonc +37 -36
  101. package/harness/openspec/config.yaml +20 -20
  102. package/harness/skills-lock.json +17 -17
  103. package/package.json +14 -12
  104. package/skills/README.md +18 -18
  105. package/skills/agent-harness-cli/SKILL.md +86 -86
  106. package/skills/browser-automation/SKILL.md +100 -72
  107. package/skills/pc-guardrails-generic/SKILL.md +53 -53
  108. package/skills/pc-guardrails-project/SKILL.md +8 -8
  109. package/skills/pc-make-architecture/SKILL.md +33 -33
  110. package/skills/pc-make-architecture/structure-template.md +64 -64
  111. package/skills/pc-make-design/SKILL.md +45 -45
  112. package/skills/pc-make-engineer/SKILL.md +59 -59
  113. package/skills/pc-make-engineer/signal-mapping.md +53 -53
  114. package/skills/pc-make-engineer/template.md +36 -36
  115. package/skills/pc-make-guardrails/SKILL.md +44 -44
  116. package/skills/pc-make-guardrails/category-reference.md +112 -112
  117. package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
  118. package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
  119. package/skills/pc-make-user-model/SKILL.md +56 -56
  120. package/skills/pc-ops-ship/SKILL.md +22 -22
  121. package/skills/pc-plan-apply/SKILL.md +90 -90
  122. package/skills/pc-plan-apply/simple-mode.md +21 -21
  123. package/skills/pc-plan-archive/SKILL.md +66 -66
  124. package/skills/pc-plan-explore/SKILL.md +21 -21
  125. package/skills/pc-plan-goal/SKILL.md +91 -91
  126. package/skills/pc-plan-goal/branching.md +39 -39
  127. package/skills/pc-plan-goal/failure-policy.md +30 -30
  128. package/skills/pc-plan-goal/output-mode.md +10 -10
  129. package/skills/pc-plan-goal/output.md +71 -71
  130. package/skills/pc-plan-propose/SKILL.md +127 -127
  131. package/skills/pc-plan-propose/task-annotation.md +39 -39
  132. package/skills/pc-plan-quick/SKILL.md +46 -46
  133. package/skills/pc-plan-story/SKILL.md +93 -93
  134. package/skills/pc-repo-audit/SKILL.md +44 -44
  135. package/skills/pc-repo-help/SKILL.md +87 -87
  136. package/skills/pc-repo-initialize/SKILL.md +119 -119
  137. package/skills/pc-repo-onboard/SKILL.md +32 -32
  138. package/skills/pc-repo-verify/SKILL.md +150 -150
  139. package/skills/pc-userstory-az/SKILL.md +71 -71
  140. package/skills/pc-userstory-browser/SKILL.md +73 -64
  141. package/skills/pc-userstory-gh/SKILL.md +63 -63
  142. package/skills/pc-userstory-jira/SKILL.md +74 -74
@@ -1,136 +1,136 @@
1
- import { execa } from "execa"
2
- import fse from "fs-extra"
3
- import path from "node:path"
4
- import { commandExists, error, header, info, loading, success, warn } from "../../utils/exec.js"
5
-
6
-
7
- async function cleanOpenspecFiles() {
8
- const cwd = process.cwd()
9
- const commandsDir = path.join(cwd, '.opencode', 'commands')
10
- const opencodeSkillsDir = path.join(cwd, '.opencode', 'skills')
11
- const agentsSkillsDir = path.join(cwd, '.agents', 'skills')
12
-
13
- // Delete opsx-* command files generated by OpenSpec: we use /plan-propose and /plan-apply instead
14
- if (await fse.pathExists(commandsDir)) {
15
- for (const f of await fse.readdir(commandsDir)) {
16
- if (f.startsWith('opsx-') && f.endsWith('.md')) {
17
- await fse.remove(path.join(commandsDir, f))
18
- info(`Removed generated command: ${f}`)
19
- }
20
- }
21
- }
22
-
23
- // Move .opencode/skills/* to .agents/skills/: skills should be in the universal location
24
- if (await fse.pathExists(opencodeSkillsDir)) {
25
- await fse.ensureDir(agentsSkillsDir)
26
- for (const dir of await fse.readdir(opencodeSkillsDir)) {
27
- const src = path.join(opencodeSkillsDir, dir)
28
- const dest = path.join(agentsSkillsDir, dir)
29
- if (!(await fse.pathExists(dest))) {
30
- await fse.move(src, dest)
31
- info(`Moved skill: ${dir} → .agents/skills/`)
32
- } else {
33
- await fse.remove(src)
34
- }
35
- }
36
- // Remove .opencode/skills if now empty
37
- const remaining = await fse.readdir(opencodeSkillsDir)
38
- if (remaining.length === 0) await fse.remove(opencodeSkillsDir)
39
- }
40
-
41
- success('OpenSpec generated files cleaned up')
42
- }
43
-
44
- export const openspecSteps = {
45
- check,
46
- install,
47
- init,
48
- }
49
-
50
- export async function initOpenspec() {
51
- header("Step 6, Initializing OpenSpec")
52
-
53
- let openSpec = await openspecSteps.check()
54
- if (!openSpec.available) {
55
- await openspecSteps.install()
56
- }
57
-
58
- openSpec = await openspecSteps.check()
59
- if (!openSpec.available) {
60
- warn("OpenSpec has not been installed. It is required to create changes.")
61
- return { ...openSpec, initialized: false }
62
- }
63
-
64
- const initResult = await openspecSteps.init()
65
- if (!initResult.initialized) {
66
- warn(
67
- "OpenSpec has not been initialized. It is required to create changes.",
68
- )
69
- return { ...openSpec, initialized: false }
70
- }
71
-
72
- await cleanOpenspecFiles()
73
- return { ...openSpec, initialized: true }
74
- }
75
-
76
- async function install() {
77
- info("Installing OpenSpec...")
78
- try {
79
- const result = await execa(
80
- "npm",
81
- ["install", "@fission-ai/openspec", "--global"],
82
- {
83
- cwd: process.cwd(),
84
- stdio: "pipe",
85
- reject: false,
86
- },
87
- )
88
- if (result.exitCode !== 0) {
89
- warn("OpenSpec install failed, check output above")
90
- }
91
- success("OpenSpec installed")
92
- } catch (err) {
93
- error(`Failed to run openspec install: ${err.message}`)
94
- }
95
- }
96
-
97
- async function check() {
98
- loading("Checking OpenSpec...")
99
-
100
- const available = await commandExists("openspec")
101
-
102
- if (available) success("OpenSpec is available")
103
- else warn("OpenSpec not found on PATH.")
104
-
105
- return { available }
106
- }
107
-
108
- async function init() {
109
- loading("Initializing OpenSpec...")
110
- try {
111
- const result = await execa(
112
- "npx",
113
- ["@fission-ai/openspec", "init", "--tools", "opencode", "--force"],
114
- {
115
- cwd: process.cwd(),
116
- reject: false,
117
- },
118
- )
119
-
120
- if (result.exitCode !== 0) {
121
- const stderr = (result.stderr || "").trim()
122
- const msg = stderr
123
- ? `init failed: ${stderr}`
124
- : `init failed with exit code ${result.exitCode}`
125
- throw new Error(msg)
126
- }
127
- } catch (err) {
128
- error(`Failed to run openspec init: ${err.message}`)
129
- return { initialized: false }
130
- }
131
-
132
- success("OpenSpec initialized")
133
- return { initialized: true }
134
- }
135
-
136
-
1
+ import { execa } from "execa"
2
+ import fse from "fs-extra"
3
+ import path from "node:path"
4
+ import { commandExists, error, header, info, loading, success, warn } from "../../utils/exec.js"
5
+
6
+
7
+ async function cleanOpenspecFiles() {
8
+ const cwd = process.cwd()
9
+ const commandsDir = path.join(cwd, '.opencode', 'commands')
10
+ const opencodeSkillsDir = path.join(cwd, '.opencode', 'skills')
11
+ const agentsSkillsDir = path.join(cwd, '.agents', 'skills')
12
+
13
+ // Delete opsx-* command files generated by OpenSpec: we use /plan-propose and /plan-apply instead
14
+ if (await fse.pathExists(commandsDir)) {
15
+ for (const f of await fse.readdir(commandsDir)) {
16
+ if (f.startsWith('opsx-') && f.endsWith('.md')) {
17
+ await fse.remove(path.join(commandsDir, f))
18
+ info(`Removed generated command: ${f}`)
19
+ }
20
+ }
21
+ }
22
+
23
+ // Move .opencode/skills/* to .agents/skills/: skills should be in the universal location
24
+ if (await fse.pathExists(opencodeSkillsDir)) {
25
+ await fse.ensureDir(agentsSkillsDir)
26
+ for (const dir of await fse.readdir(opencodeSkillsDir)) {
27
+ const src = path.join(opencodeSkillsDir, dir)
28
+ const dest = path.join(agentsSkillsDir, dir)
29
+ if (!(await fse.pathExists(dest))) {
30
+ await fse.move(src, dest)
31
+ info(`Moved skill: ${dir} → .agents/skills/`)
32
+ } else {
33
+ await fse.remove(src)
34
+ }
35
+ }
36
+ // Remove .opencode/skills if now empty
37
+ const remaining = await fse.readdir(opencodeSkillsDir)
38
+ if (remaining.length === 0) await fse.remove(opencodeSkillsDir)
39
+ }
40
+
41
+ success('OpenSpec generated files cleaned up')
42
+ }
43
+
44
+ export const openspecSteps = {
45
+ check,
46
+ install,
47
+ init,
48
+ }
49
+
50
+ export async function initOpenspec() {
51
+ header("Step 6, Initializing OpenSpec")
52
+
53
+ let openSpec = await openspecSteps.check()
54
+ if (!openSpec.available) {
55
+ await openspecSteps.install()
56
+ }
57
+
58
+ openSpec = await openspecSteps.check()
59
+ if (!openSpec.available) {
60
+ warn("OpenSpec has not been installed. It is required to create changes.")
61
+ return { ...openSpec, initialized: false }
62
+ }
63
+
64
+ const initResult = await openspecSteps.init()
65
+ if (!initResult.initialized) {
66
+ warn(
67
+ "OpenSpec has not been initialized. It is required to create changes.",
68
+ )
69
+ return { ...openSpec, initialized: false }
70
+ }
71
+
72
+ await cleanOpenspecFiles()
73
+ return { ...openSpec, initialized: true }
74
+ }
75
+
76
+ async function install() {
77
+ info("Installing OpenSpec...")
78
+ try {
79
+ const result = await execa(
80
+ "npm",
81
+ ["install", "@fission-ai/openspec", "--global"],
82
+ {
83
+ cwd: process.cwd(),
84
+ stdio: "pipe",
85
+ reject: false,
86
+ },
87
+ )
88
+ if (result.exitCode !== 0) {
89
+ warn("OpenSpec install failed, check output above")
90
+ }
91
+ success("OpenSpec installed")
92
+ } catch (err) {
93
+ error(`Failed to run openspec install: ${err.message}`)
94
+ }
95
+ }
96
+
97
+ async function check() {
98
+ loading("Checking OpenSpec...")
99
+
100
+ const available = await commandExists("openspec")
101
+
102
+ if (available) success("OpenSpec is available")
103
+ else warn("OpenSpec not found on PATH.")
104
+
105
+ return { available }
106
+ }
107
+
108
+ async function init() {
109
+ loading("Initializing OpenSpec...")
110
+ try {
111
+ const result = await execa(
112
+ "npx",
113
+ ["@fission-ai/openspec", "init", "--tools", "opencode", "--force"],
114
+ {
115
+ cwd: process.cwd(),
116
+ reject: false,
117
+ },
118
+ )
119
+
120
+ if (result.exitCode !== 0) {
121
+ const stderr = (result.stderr || "").trim()
122
+ const msg = stderr
123
+ ? `init failed: ${stderr}`
124
+ : `init failed with exit code ${result.exitCode}`
125
+ throw new Error(msg)
126
+ }
127
+ } catch (err) {
128
+ error(`Failed to run openspec init: ${err.message}`)
129
+ return { initialized: false }
130
+ }
131
+
132
+ success("OpenSpec initialized")
133
+ return { initialized: true }
134
+ }
135
+
136
+
@@ -1,141 +1,141 @@
1
- import { execa } from 'execa'
2
- import fse from 'fs-extra'
3
- import path from 'node:path'
4
- import os from 'node:os'
5
- import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser'
6
- import { header, success, warn, error, loading } from '../../utils/exec.js'
7
-
8
- /**
9
- * Normalizes CodeGraph's MCP command in the selected OpenCode config.
10
- * Returns true if the config was successfully normalized (or no config exists).
11
- */
12
- export async function fixCodegraphConfig(location = 'local') {
13
- const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim()
14
- const configDir = location === 'global'
15
- ? path.join(xdgConfigHome || path.join(os.homedir(), '.config'), 'opencode')
16
- : process.cwd()
17
- const jsoncFile = path.join(configDir, 'opencode.jsonc')
18
- const jsonFile = path.join(configDir, 'opencode.json')
19
- const configFile = await fse.pathExists(jsoncFile)
20
- ? jsoncFile
21
- : await fse.pathExists(jsonFile)
22
- ? jsonFile
23
- : jsoncFile
24
-
25
- if (!await fse.pathExists(configFile)) return true
26
-
27
- let configText
28
- let config
29
- try {
30
- configText = await fse.readFile(configFile, 'utf-8')
31
- const errors = []
32
- config = parseJsonc(configText, errors)
33
- if (errors.length > 0) throw new Error(`parse errors: ${errors.length}`)
34
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
35
- throw new Error('unexpected structure')
36
- }
37
- } catch {
38
- warn('Could not parse opencode.jsonc, leaving it untouched')
39
- return false
40
- }
41
-
42
- const entry = config.mcp?.servers?.codegraph
43
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return true
44
-
45
- const normalizedEntry = { ...entry }
46
- let changed = false
47
-
48
- if (normalizedEntry.type == null) {
49
- normalizedEntry.type = 'local'
50
- changed = true
51
- }
52
- if (Array.isArray(normalizedEntry.command) && normalizedEntry.command[0] === 'codegraph') {
53
- normalizedEntry.command = ['npx', '--yes', '@colbymchenry/codegraph', ...normalizedEntry.command.slice(1)]
54
- changed = true
55
- }
56
- if (normalizedEntry.timeout == null) {
57
- normalizedEntry.timeout = {
58
- catalog: 120000,
59
- execution: 120000,
60
- }
61
- changed = true
62
- } else if (typeof normalizedEntry.timeout !== 'object' || Array.isArray(normalizedEntry.timeout)) {
63
- const timeout = Number.isFinite(normalizedEntry.timeout) && normalizedEntry.timeout > 0
64
- ? normalizedEntry.timeout
65
- : 120000
66
- normalizedEntry.timeout = { catalog: timeout, execution: timeout }
67
- changed = true
68
- }
69
- if (normalizedEntry.codemode !== false) {
70
- normalizedEntry.codemode = false
71
- changed = true
72
- }
73
-
74
- if (changed) {
75
- configText = applyEdits(configText, modify(configText, ['mcp', 'servers', 'codegraph'], normalizedEntry, {
76
- formattingOptions: { insertSpaces: true, tabSize: 2 },
77
- }))
78
- await fse.writeFile(configFile, configText, 'utf-8')
79
- warn('Normalized codegraph MCP config in opencode.jsonc')
80
- }
81
-
82
- return true
83
- }
84
-
85
- export async function installCodegraph(options = {}) {
86
- if (!options.skipHeader) header('Installing codegraph')
87
-
88
- const location = options.installScope === 'global' ? 'global' : 'local'
89
-
90
- loading(`configuring codegraph for opencode (${location})...`)
91
-
92
- try {
93
- const installResult = await execa(
94
- 'npx',
95
- ['--yes', '@colbymchenry/codegraph', 'install', '--target=opencode', `--location=${location}`, '--yes'],
96
- {
97
- cwd: process.cwd(),
98
- reject: false,
99
- stdio: 'pipe',
100
- }
101
- )
102
-
103
- if (installResult.exitCode !== 0) {
104
- warn('codegraph install exited with non-zero code')
105
- return { optedIn: true, installed: false }
106
- }
107
-
108
- const configFixed = await fixCodegraphConfig(location)
109
-
110
- if (!configFixed) {
111
- warn('codegraph config could not be merged: skipping init')
112
- return { optedIn: true, installed: false }
113
- }
114
-
115
- success(`codegraph configured for opencode (${location})`)
116
- } catch (err) {
117
- error(`Failed to install codegraph: ${err.message}`)
118
- return { optedIn: true, installed: false }
119
- }
120
-
121
- loading('initializing codegraph project index...')
122
-
123
- try {
124
- const initResult = await execa('npx', ['@colbymchenry/codegraph', 'init', '-i'], {
125
- cwd: process.cwd(),
126
- reject: false,
127
- stdio: 'pipe',
128
- })
129
-
130
- if (initResult.exitCode !== 0) {
131
- warn('codegraph init exited with non-zero code')
132
- return { optedIn: true, installed: false }
133
- }
134
- success('codegraph project index initialized')
135
- } catch (err) {
136
- error(`Failed to initialize codegraph: ${err.message}`)
137
- return { optedIn: true, installed: false }
138
- }
139
-
140
- return { optedIn: true, installed: true }
141
- }
1
+ import { execa } from 'execa'
2
+ import fse from 'fs-extra'
3
+ import path from 'node:path'
4
+ import os from 'node:os'
5
+ import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser'
6
+ import { header, success, warn, error, loading } from '../../utils/exec.js'
7
+
8
+ /**
9
+ * Normalizes CodeGraph's MCP command in the selected OpenCode config.
10
+ * Returns true if the config was successfully normalized (or no config exists).
11
+ */
12
+ export async function fixCodegraphConfig(location = 'local') {
13
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim()
14
+ const configDir = location === 'global'
15
+ ? path.join(xdgConfigHome || path.join(os.homedir(), '.config'), 'opencode')
16
+ : process.cwd()
17
+ const jsoncFile = path.join(configDir, 'opencode.jsonc')
18
+ const jsonFile = path.join(configDir, 'opencode.json')
19
+ const configFile = await fse.pathExists(jsoncFile)
20
+ ? jsoncFile
21
+ : await fse.pathExists(jsonFile)
22
+ ? jsonFile
23
+ : jsoncFile
24
+
25
+ if (!await fse.pathExists(configFile)) return true
26
+
27
+ let configText
28
+ let config
29
+ try {
30
+ configText = await fse.readFile(configFile, 'utf-8')
31
+ const errors = []
32
+ config = parseJsonc(configText, errors)
33
+ if (errors.length > 0) throw new Error(`parse errors: ${errors.length}`)
34
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
35
+ throw new Error('unexpected structure')
36
+ }
37
+ } catch {
38
+ warn('Could not parse opencode.jsonc, leaving it untouched')
39
+ return false
40
+ }
41
+
42
+ const entry = config.mcp?.servers?.codegraph
43
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return true
44
+
45
+ const normalizedEntry = { ...entry }
46
+ let changed = false
47
+
48
+ if (normalizedEntry.type == null) {
49
+ normalizedEntry.type = 'local'
50
+ changed = true
51
+ }
52
+ if (Array.isArray(normalizedEntry.command) && normalizedEntry.command[0] === 'codegraph') {
53
+ normalizedEntry.command = ['npx', '--yes', '@colbymchenry/codegraph', ...normalizedEntry.command.slice(1)]
54
+ changed = true
55
+ }
56
+ if (normalizedEntry.timeout == null) {
57
+ normalizedEntry.timeout = {
58
+ catalog: 120000,
59
+ execution: 120000,
60
+ }
61
+ changed = true
62
+ } else if (typeof normalizedEntry.timeout !== 'object' || Array.isArray(normalizedEntry.timeout)) {
63
+ const timeout = Number.isFinite(normalizedEntry.timeout) && normalizedEntry.timeout > 0
64
+ ? normalizedEntry.timeout
65
+ : 120000
66
+ normalizedEntry.timeout = { catalog: timeout, execution: timeout }
67
+ changed = true
68
+ }
69
+ if (normalizedEntry.codemode !== false) {
70
+ normalizedEntry.codemode = false
71
+ changed = true
72
+ }
73
+
74
+ if (changed) {
75
+ configText = applyEdits(configText, modify(configText, ['mcp', 'servers', 'codegraph'], normalizedEntry, {
76
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
77
+ }))
78
+ await fse.writeFile(configFile, configText, 'utf-8')
79
+ warn('Normalized codegraph MCP config in opencode.jsonc')
80
+ }
81
+
82
+ return true
83
+ }
84
+
85
+ export async function installCodegraph(options = {}) {
86
+ if (!options.skipHeader) header('Installing codegraph')
87
+
88
+ const location = options.installScope === 'global' ? 'global' : 'local'
89
+
90
+ loading(`configuring codegraph for opencode (${location})...`)
91
+
92
+ try {
93
+ const installResult = await execa(
94
+ 'npx',
95
+ ['--yes', '@colbymchenry/codegraph', 'install', '--target=opencode', `--location=${location}`, '--yes'],
96
+ {
97
+ cwd: process.cwd(),
98
+ reject: false,
99
+ stdio: 'pipe',
100
+ }
101
+ )
102
+
103
+ if (installResult.exitCode !== 0) {
104
+ warn('codegraph install exited with non-zero code')
105
+ return { optedIn: true, installed: false }
106
+ }
107
+
108
+ const configFixed = await fixCodegraphConfig(location)
109
+
110
+ if (!configFixed) {
111
+ warn('codegraph config could not be merged: skipping init')
112
+ return { optedIn: true, installed: false }
113
+ }
114
+
115
+ success(`codegraph configured for opencode (${location})`)
116
+ } catch (err) {
117
+ error(`Failed to install codegraph: ${err.message}`)
118
+ return { optedIn: true, installed: false }
119
+ }
120
+
121
+ loading('initializing codegraph project index...')
122
+
123
+ try {
124
+ const initResult = await execa('npx', ['@colbymchenry/codegraph', 'init', '-i'], {
125
+ cwd: process.cwd(),
126
+ reject: false,
127
+ stdio: 'pipe',
128
+ })
129
+
130
+ if (initResult.exitCode !== 0) {
131
+ warn('codegraph init exited with non-zero code')
132
+ return { optedIn: true, installed: false }
133
+ }
134
+ success('codegraph project index initialized')
135
+ } catch (err) {
136
+ error(`Failed to initialize codegraph: ${err.message}`)
137
+ return { optedIn: true, installed: false }
138
+ }
139
+
140
+ return { optedIn: true, installed: true }
141
+ }