@plainconceptsplatform/agent-harness 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +419 -0
- package/package.json +68 -0
- package/src/commands/join.js +244 -0
- package/src/commands/shared.js +27 -0
- package/src/commands/single.js +79 -0
- package/src/commands/update.js +109 -0
- package/src/commands/wizard.js +134 -0
- package/src/content/.agents/skills/browser-automation/SKILL.md +66 -0
- package/src/content/.agents/skills/pc-guardrails-generic/SKILL.md +68 -0
- package/src/content/.agents/skills/pc-guardrails-project/SKILL.md +8 -0
- package/src/content/.agents/skills/pc-make-architecture/SKILL.md +51 -0
- package/src/content/.agents/skills/pc-make-architecture/structure-template.md +38 -0
- package/src/content/.agents/skills/pc-make-design/SKILL.md +68 -0
- package/src/content/.agents/skills/pc-make-engineer/SKILL.md +219 -0
- package/src/content/.agents/skills/pc-make-engineer/signal-mapping.md +68 -0
- package/src/content/.agents/skills/pc-make-engineer/template.md +81 -0
- package/src/content/.agents/skills/pc-make-evidence-scaffold/SKILL.md +18 -0
- package/src/content/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +29 -0
- package/src/content/.agents/skills/pc-make-guardrails/SKILL.md +74 -0
- package/src/content/.agents/skills/pc-make-guardrails/category-reference.md +68 -0
- package/src/content/.agents/skills/pc-make-merge-risk-assess/SKILL.md +70 -0
- package/src/content/.agents/skills/pc-make-merge-risk-assess/category-reference.md +98 -0
- package/src/content/.agents/skills/pc-make-user-model/SKILL.md +66 -0
- package/src/content/.agents/skills/pc-ops-evidence/SKILL.md +127 -0
- package/src/content/.agents/skills/pc-ops-ship/SKILL.md +18 -0
- package/src/content/.agents/skills/pc-plan-apply/SKILL.md +83 -0
- package/src/content/.agents/skills/pc-plan-apply/simple-mode.md +21 -0
- package/src/content/.agents/skills/pc-plan-archive/SKILL.md +63 -0
- package/src/content/.agents/skills/pc-plan-explore/SKILL.md +9 -0
- package/src/content/.agents/skills/pc-plan-goal/SKILL.md +94 -0
- package/src/content/.agents/skills/pc-plan-goal/branching.md +30 -0
- package/src/content/.agents/skills/pc-plan-goal/failure-policy.md +30 -0
- package/src/content/.agents/skills/pc-plan-goal/output-mode.md +9 -0
- package/src/content/.agents/skills/pc-plan-goal/output.md +68 -0
- package/src/content/.agents/skills/pc-plan-propose/SKILL.md +125 -0
- package/src/content/.agents/skills/pc-plan-propose/task-annotation.md +39 -0
- package/src/content/.agents/skills/pc-plan-quick/SKILL.md +62 -0
- package/src/content/.agents/skills/pc-plan-story/SKILL.md +146 -0
- package/src/content/.agents/skills/pc-repo-audit/SKILL.md +44 -0
- package/src/content/.agents/skills/pc-repo-help/SKILL.md +91 -0
- package/src/content/.agents/skills/pc-repo-initialize/SKILL.md +130 -0
- package/src/content/.agents/skills/pc-repo-onboard/SKILL.md +87 -0
- package/src/content/.agents/skills/pc-repo-verify/SKILL.md +34 -0
- package/src/content/.agents/skills/pc-userstory-az/SKILL.md +157 -0
- package/src/content/.agents/skills/pc-userstory-browser/SKILL.md +132 -0
- package/src/content/.agents/skills/pc-userstory-gh/SKILL.md +120 -0
- package/src/content/.agents/skills/pc-userstory-jira/SKILL.md +131 -0
- package/src/content/.opencode/_gitignore +7 -0
- package/src/content/.opencode/commands/init.md +5 -0
- package/src/content/.opencode/commands/make-architecture.md +5 -0
- package/src/content/.opencode/commands/make-design.md +5 -0
- package/src/content/.opencode/commands/make-engineer.md +5 -0
- package/src/content/.opencode/commands/make-evidence-scaffold.md +5 -0
- package/src/content/.opencode/commands/make-guardrails.md +5 -0
- package/src/content/.opencode/commands/make-user-model.md +5 -0
- package/src/content/.opencode/commands/ops-backlog.md +10 -0
- package/src/content/.opencode/commands/ops-evidence.md +9 -0
- package/src/content/.opencode/commands/ops-review.md +8 -0
- package/src/content/.opencode/commands/ops-ship.md +9 -0
- package/src/content/.opencode/commands/plan-apply.md +9 -0
- package/src/content/.opencode/commands/plan-archive.md +5 -0
- package/src/content/.opencode/commands/plan-explore.md +9 -0
- package/src/content/.opencode/commands/plan-goal.md +5 -0
- package/src/content/.opencode/commands/plan-propose.md +9 -0
- package/src/content/.opencode/commands/plan-quick.md +5 -0
- package/src/content/.opencode/commands/plan-story.md +9 -0
- package/src/content/.opencode/commands/repo-audit.md +5 -0
- package/src/content/.opencode/commands/repo-help.md +5 -0
- package/src/content/.opencode/commands/repo-initialize.md +5 -0
- package/src/content/.opencode/commands/repo-onboard.md +5 -0
- package/src/content/.opencode/commands/repo-verify.md +5 -0
- package/src/content/.opencode/package.json +10 -0
- package/src/content/.opencode/plugins/pc-subagent-monitor.js +139 -0
- package/src/content/.opencode/plugins/pc-subagent-tiers.js +179 -0
- package/src/content/.opencode/plugins/pc-system-reminders.js +96 -0
- package/src/content/.opencode/plugins/pc-system-reminders.test.js +35 -0
- package/src/content/.opencode/tui/pc-subagents.tsx +98 -0
- package/src/content/.opencode/tui.json +6 -0
- package/src/content/AGENTS.md +71 -0
- package/src/content/ARCHITECTURE.md +16 -0
- package/src/content/DESIGN.md +16 -0
- package/src/content/opencode.jsonc +31 -0
- package/src/content/openspec/changes/archive/.gitkeep +0 -0
- package/src/content/openspec/config.yaml +20 -0
- package/src/content/openspec/specs/.gitkeep +0 -0
- package/src/content/skills-lock.json +17 -0
- package/src/fragments/archive/az.md +95 -0
- package/src/fragments/archive/gh.md +94 -0
- package/src/fragments/archive/gl.md +94 -0
- package/src/fragments/archive/none.md +73 -0
- package/src/fragments/guardrails/codegraph.md +7 -0
- package/src/fragments/guardrails/humanizer.md +4 -0
- package/src/fragments/guardrails/memory.md +4 -0
- package/src/fragments/guardrails/rtk.md +3 -0
- package/src/fragments/guardrails/simple-english.md +4 -0
- package/src/fragments/ops-backlog/az.md +29 -0
- package/src/fragments/ops-backlog/gh.md +30 -0
- package/src/fragments/ops-backlog/jira.md +29 -0
- package/src/fragments/ops-evidence/az.md +41 -0
- package/src/fragments/ops-evidence/gh.md +53 -0
- package/src/fragments/ops-evidence/jira.md +38 -0
- package/src/fragments/ops-review/az.md +63 -0
- package/src/fragments/ops-review/gh.md +53 -0
- package/src/fragments/ops-review/gl.md +57 -0
- package/src/fragments/ops-ship/az.md +81 -0
- package/src/fragments/ops-ship/gh.md +69 -0
- package/src/fragments/ops-ship/gl.md +86 -0
- package/src/index.js +107 -0
- package/src/presets/agents-content.json +53 -0
- package/src/presets/browser.json +22 -0
- package/src/presets/clean.json +21 -0
- package/src/presets/models.json +68 -0
- package/src/presets/openspec.json +1 -0
- package/src/presets/optimization.json +37 -0
- package/src/presets/platforms.json +76 -0
- package/src/presets/quota.json +16 -0
- package/src/presets/source.json +23 -0
- package/src/steps/browser/index.js +91 -0
- package/src/steps/clean/index.js +120 -0
- package/src/steps/copy/agents.js +118 -0
- package/src/steps/copy/commands.js +91 -0
- package/src/steps/copy/fullstack-engineer.js +83 -0
- package/src/steps/copy/index.js +88 -0
- package/src/steps/copy/opencode-json.js +129 -0
- package/src/steps/copy/skills.js +196 -0
- package/src/steps/metadata/index.js +108 -0
- package/src/steps/models/format.js +88 -0
- package/src/steps/models/index.js +64 -0
- package/src/steps/models/write.js +34 -0
- package/src/steps/openspec/index.js +136 -0
- package/src/steps/optimization/codegraph.js +127 -0
- package/src/steps/optimization/humanizer.js +17 -0
- package/src/steps/optimization/index.js +163 -0
- package/src/steps/optimization/memory.js +88 -0
- package/src/steps/optimization/patch-guardrails.js +108 -0
- package/src/steps/optimization/quota.js +119 -0
- package/src/steps/optimization/simple-english.js +17 -0
- package/src/steps/optimization/skills-lock.js +30 -0
- package/src/steps/platform/index.js +109 -0
- package/src/steps/source/index.js +123 -0
- package/src/utils/copy.js +108 -0
- package/src/utils/exec-spinner.js +47 -0
- package/src/utils/exec.js +134 -0
- package/src/utils/legacy-check.js +30 -0
- package/src/utils/models-cache.js +58 -0
- package/src/utils/models-pricing.js +42 -0
- package/src/utils/paths.js +64 -0
- package/src/utils/process.js +3 -0
- package/src/utils/terminal.js +6 -0
- package/src/utils/update-manifest.js +49 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser'
|
|
5
|
+
import { header, success, warn, error, loading } from '../../utils/exec.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalizes codegraph's MCP entry in the project-level OpenCode config.
|
|
9
|
+
* Older codegraph versions write `mcpServers`; OpenCode expects `mcp`.
|
|
10
|
+
* Returns true if the config was successfully normalized (or no config exists).
|
|
11
|
+
*/
|
|
12
|
+
export async function fixCodegraphConfig() {
|
|
13
|
+
const cwd = process.cwd()
|
|
14
|
+
const configFile = path.join(cwd, 'opencode.jsonc')
|
|
15
|
+
|
|
16
|
+
if (!await fse.pathExists(configFile)) return true
|
|
17
|
+
|
|
18
|
+
let configText
|
|
19
|
+
let config
|
|
20
|
+
try {
|
|
21
|
+
configText = await fse.readFile(configFile, 'utf-8')
|
|
22
|
+
const errors = []
|
|
23
|
+
config = parseJsonc(configText, errors)
|
|
24
|
+
if (errors.length > 0) throw new Error(`parse errors: ${errors.length}`)
|
|
25
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
26
|
+
throw new Error('unexpected structure')
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
warn('Could not parse opencode.jsonc, leaving it untouched')
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const legacyMcp = config.mcpServers
|
|
34
|
+
const mcp = config.mcp || {}
|
|
35
|
+
const entries = { ...legacyMcp, ...mcp }
|
|
36
|
+
let changed = false
|
|
37
|
+
|
|
38
|
+
if (Object.keys(entries).length > 0) {
|
|
39
|
+
for (const entry of Object.values(entries)) {
|
|
40
|
+
if (Array.isArray(entry.command) && entry.command[0] === 'codegraph') {
|
|
41
|
+
entry.command = ['npx', '@colbymchenry/codegraph', ...entry.command.slice(1)]
|
|
42
|
+
// codegraph defaults to a 30s MCP timeout, which fails under parallel
|
|
43
|
+
// subagent load. Raise it unless the user has set one explicitly.
|
|
44
|
+
if (entry.timeout == null) entry.timeout = 120000
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
48
|
+
const edits = modify(configText, ['mcp', name], entry, {
|
|
49
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
50
|
+
})
|
|
51
|
+
configText = applyEdits(configText, edits)
|
|
52
|
+
changed = true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (legacyMcp) {
|
|
57
|
+
configText = applyEdits(configText, modify(configText, ['mcpServers'], undefined, {
|
|
58
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
59
|
+
}))
|
|
60
|
+
changed = true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (changed) {
|
|
64
|
+
await fse.writeFile(configFile, configText, 'utf-8')
|
|
65
|
+
warn('Normalized codegraph MCP config in opencode.jsonc')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function installCodegraph(options = {}) {
|
|
72
|
+
if (!options.skipHeader) header('Installing codegraph')
|
|
73
|
+
|
|
74
|
+
const location = options.installScope === 'global' ? 'global' : 'local'
|
|
75
|
+
|
|
76
|
+
loading(`configuring codegraph for opencode (${location})...`)
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const installResult = await execa(
|
|
80
|
+
'npx',
|
|
81
|
+
['--yes', '@colbymchenry/codegraph', 'install', '--target=opencode', `--location=${location}`, '--yes'],
|
|
82
|
+
{
|
|
83
|
+
cwd: process.cwd(),
|
|
84
|
+
reject: false,
|
|
85
|
+
stdio: 'pipe',
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (installResult.exitCode !== 0) {
|
|
90
|
+
warn('codegraph install exited with non-zero code')
|
|
91
|
+
return { optedIn: true, installed: false }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const configFixed = await fixCodegraphConfig()
|
|
95
|
+
|
|
96
|
+
if (!configFixed) {
|
|
97
|
+
warn('codegraph config could not be merged: skipping init')
|
|
98
|
+
return { optedIn: true, installed: false }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
success(`codegraph configured for opencode (${location})`)
|
|
102
|
+
} catch (err) {
|
|
103
|
+
error(`Failed to install codegraph: ${err.message}`)
|
|
104
|
+
return { optedIn: true, installed: false }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
loading('initializing codegraph project index...')
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const initResult = await execa('npx', ['@colbymchenry/codegraph', 'init', '-i'], {
|
|
111
|
+
cwd: process.cwd(),
|
|
112
|
+
reject: false,
|
|
113
|
+
stdio: 'pipe',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
if (initResult.exitCode !== 0) {
|
|
117
|
+
warn('codegraph init exited with non-zero code')
|
|
118
|
+
return { optedIn: true, installed: false }
|
|
119
|
+
}
|
|
120
|
+
success('codegraph project index initialized')
|
|
121
|
+
} catch (err) {
|
|
122
|
+
error(`Failed to initialize codegraph: ${err.message}`)
|
|
123
|
+
return { optedIn: true, installed: false }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { optedIn: true, installed: true }
|
|
127
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { addSkillToLock } from './skills-lock.js'
|
|
2
|
+
import { header, success, info } from '../../utils/exec.js'
|
|
3
|
+
|
|
4
|
+
const SKILL_ENTRY = {
|
|
5
|
+
source: 'blader/humanizer',
|
|
6
|
+
sourceType: 'github',
|
|
7
|
+
skillPath: 'SKILL.md',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function installHumanizer(options = {}) {
|
|
11
|
+
if (!options.skipHeader) header('Installing humanizer')
|
|
12
|
+
|
|
13
|
+
info('Adding humanizer to skills-lock.json for batch install')
|
|
14
|
+
await addSkillToLock('humanizer', SKILL_ENTRY)
|
|
15
|
+
success('humanizer queued for installation')
|
|
16
|
+
return { optedIn: true, installed: true }
|
|
17
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { checkbox, confirm } from '@inquirer/prompts'
|
|
2
|
+
import { execa } from 'execa'
|
|
3
|
+
import fse from 'fs-extra'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import { code, commandExists, header, info, loading, success, warn } from '../../utils/exec.js'
|
|
7
|
+
import { installQuota } from './quota.js'
|
|
8
|
+
import { installSimpleEnglish } from './simple-english.js'
|
|
9
|
+
import { installCodegraph } from './codegraph.js'
|
|
10
|
+
import { installMemory } from './memory.js'
|
|
11
|
+
import { installHumanizer } from './humanizer.js'
|
|
12
|
+
import { patchGuardrails } from './patch-guardrails.js'
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
15
|
+
const OPTIMIZATION_PRESET_PATH = path.resolve(__dirname, '../../presets/optimization.json')
|
|
16
|
+
const optimizationPreset = await fse.readJson(OPTIMIZATION_PRESET_PATH)
|
|
17
|
+
|
|
18
|
+
export async function checkRtk(options = {}) {
|
|
19
|
+
if (!options.skipHeader) header('Checking rtk')
|
|
20
|
+
|
|
21
|
+
let shouldCheck = true
|
|
22
|
+
if (!options.skipPrompt) {
|
|
23
|
+
info('Recommended: install and verify rtk for safer agent CLI command execution.')
|
|
24
|
+
shouldCheck = await confirm({
|
|
25
|
+
message: 'Check rtk now?',
|
|
26
|
+
default: true,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!shouldCheck) {
|
|
31
|
+
warn('Skipped rtk check (you can install it later)')
|
|
32
|
+
return { optedIn: false, checked: false, available: false }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
loading('checking rtk...')
|
|
36
|
+
|
|
37
|
+
const available = await commandExists('rtk')
|
|
38
|
+
|
|
39
|
+
if (available) {
|
|
40
|
+
success('rtk is available')
|
|
41
|
+
return { optedIn: true, checked: true, available: true }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
warn('rtk not found on PATH.')
|
|
45
|
+
console.log()
|
|
46
|
+
info('rtk is required for agents to run CLI commands safely.')
|
|
47
|
+
info('Install it from: https://github.com/rtk-ai/rtk#pre-built-binaries')
|
|
48
|
+
console.log()
|
|
49
|
+
info('After installing, verify with:')
|
|
50
|
+
code(['rtk --version'])
|
|
51
|
+
return { optedIn: true, checked: true, available: false }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function tokenOptimizationStep(options = {}) {
|
|
55
|
+
header('Step 8, Token optimization tools')
|
|
56
|
+
|
|
57
|
+
const defaultSelected = optimizationPreset.choices
|
|
58
|
+
.filter(choice => choice.checked)
|
|
59
|
+
.map(choice => choice.value)
|
|
60
|
+
let selected = defaultSelected
|
|
61
|
+
|
|
62
|
+
if (!options.skipPrompt && process.stdin.isTTY) {
|
|
63
|
+
info(optimizationPreset.info)
|
|
64
|
+
// Abort the prompt via signal instead of racing it: a raced prompt keeps
|
|
65
|
+
// stdin in raw mode after "losing" and rejects unhandled on a later Ctrl+C.
|
|
66
|
+
const controller = new AbortController()
|
|
67
|
+
const timer = setTimeout(() => controller.abort(), optimizationPreset.timeoutMs)
|
|
68
|
+
try {
|
|
69
|
+
selected = await checkbox(
|
|
70
|
+
{
|
|
71
|
+
message: optimizationPreset.message,
|
|
72
|
+
choices: optimizationPreset.choices,
|
|
73
|
+
},
|
|
74
|
+
{ signal: controller.signal },
|
|
75
|
+
)
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err.name !== 'AbortPromptError') throw err
|
|
78
|
+
selected = defaultSelected
|
|
79
|
+
info('No response: continuing with the recommended defaults.')
|
|
80
|
+
} finally {
|
|
81
|
+
clearTimeout(timer)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
loading('applying token optimization selections...')
|
|
86
|
+
|
|
87
|
+
const installScope = options.ctx?.installScope || 'local'
|
|
88
|
+
|
|
89
|
+
const has = value => selected.includes(value)
|
|
90
|
+
|
|
91
|
+
const rtk = has('rtk')
|
|
92
|
+
? await checkRtk({ skipHeader: true, skipPrompt: true })
|
|
93
|
+
: { optedIn: false, checked: false, available: false }
|
|
94
|
+
|
|
95
|
+
const quota = has('quota')
|
|
96
|
+
? await installQuota({ skipHeader: true, skipPrompt: true })
|
|
97
|
+
: { optedIn: false, installed: false }
|
|
98
|
+
|
|
99
|
+
const simpleEnglish = has('simpleEnglish')
|
|
100
|
+
? await installSimpleEnglish({
|
|
101
|
+
skipHeader: true,
|
|
102
|
+
skipPrompt: true,
|
|
103
|
+
installScope,
|
|
104
|
+
})
|
|
105
|
+
: { optedIn: false, installed: false }
|
|
106
|
+
|
|
107
|
+
const codegraph = has('codegraph')
|
|
108
|
+
? await installCodegraph({ skipHeader: true, installScope })
|
|
109
|
+
: { optedIn: false, installed: false }
|
|
110
|
+
|
|
111
|
+
const memory = has('memory')
|
|
112
|
+
? await installMemory({ skipHeader: true })
|
|
113
|
+
: { optedIn: false, installed: false }
|
|
114
|
+
|
|
115
|
+
const humanizer = has('humanizer')
|
|
116
|
+
? await installHumanizer({ skipHeader: true, installScope })
|
|
117
|
+
: { optedIn: false, installed: false }
|
|
118
|
+
|
|
119
|
+
// Run a single npx skills experimental_install to pick up all skills
|
|
120
|
+
// that were added to skills-lock.json by the individual installers above.
|
|
121
|
+
await syncSkillsLock()
|
|
122
|
+
|
|
123
|
+
// Patch guardrails skill with selected tool guidance sections
|
|
124
|
+
await patchGuardrails({
|
|
125
|
+
rtk: rtk.available,
|
|
126
|
+
codegraph: codegraph.optedIn,
|
|
127
|
+
memory: memory.optedIn,
|
|
128
|
+
simpleEnglish: simpleEnglish.installed,
|
|
129
|
+
humanizer: humanizer.optedIn,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if (selected.length === 0) warn('No token optimization tools selected')
|
|
133
|
+
else success('Token optimization step completed')
|
|
134
|
+
|
|
135
|
+
return { rtk, quota, simpleEnglish, codegraph, memory, humanizer }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function syncSkillsLock() {
|
|
139
|
+
const destLock = path.join(process.cwd(), 'skills-lock.json')
|
|
140
|
+
if (!await fse.pathExists(destLock)) {
|
|
141
|
+
info('No skills-lock.json found, skipping skills sync')
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
loading('syncing skills via npx skills experimental_install...')
|
|
146
|
+
try {
|
|
147
|
+
const result = await execa('npx', ['skills', 'experimental_install', '--yes'], {
|
|
148
|
+
cwd: process.cwd(),
|
|
149
|
+
reject: false,
|
|
150
|
+
stdio: 'pipe',
|
|
151
|
+
timeout: 600000,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
if (result.exitCode === 0) {
|
|
155
|
+
success('Skills synced from skills-lock.json')
|
|
156
|
+
} else {
|
|
157
|
+
const errLine = result.stderr?.trim().split('\n').slice(-3).join('\n')
|
|
158
|
+
warn(`npx skills experimental_install exited with non-zero code${errLine ? `: ${errLine}` : ''}`)
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
warn(`npx skills experimental_install failed: ${err.message}`)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import { addSkillToLock } from './skills-lock.js'
|
|
3
|
+
import fse from 'fs-extra'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser'
|
|
6
|
+
import { error, info, loading, success, warn } from '../../utils/exec.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Configures the agentmemory MCP server in opencode.jsonc
|
|
10
|
+
* and installs the agentmemory skills.
|
|
11
|
+
*
|
|
12
|
+
* agentmemory runs as a persistent local server on localhost:3111.
|
|
13
|
+
* Requires Node.js 18+ (npm install -g @agentmemory/agentmemory).
|
|
14
|
+
* MCP bridge: npx -y @agentmemory/mcp (proxies to the running server).
|
|
15
|
+
* Skill source: https://github.com/rohitg00/agentmemory
|
|
16
|
+
*/
|
|
17
|
+
export async function installMemory(options = {}) {
|
|
18
|
+
if (!options.skipHeader) info('Configuring agentmemory local memory server...')
|
|
19
|
+
|
|
20
|
+
// Install agentmemory globally so the `agentmemory` command is on PATH
|
|
21
|
+
loading('installing agentmemory (this may take a minute)...')
|
|
22
|
+
try {
|
|
23
|
+
const installResult = await execa('npm', ['install', '-g', '@agentmemory/agentmemory'], {
|
|
24
|
+
reject: false,
|
|
25
|
+
timeout: 300000,
|
|
26
|
+
stdio: 'pipe',
|
|
27
|
+
})
|
|
28
|
+
if (installResult.exitCode === 0) {
|
|
29
|
+
success('agentmemory installed')
|
|
30
|
+
} else {
|
|
31
|
+
warn('agentmemory install exited with non-zero code, MCP may fall back to npx resolution')
|
|
32
|
+
}
|
|
33
|
+
} catch (err) {
|
|
34
|
+
warn(`agentmemory install failed: ${err.message}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Configure MCP server in opencode.jsonc
|
|
38
|
+
try {
|
|
39
|
+
const opencodePath = path.join(process.cwd(), 'opencode.jsonc')
|
|
40
|
+
|
|
41
|
+
let opencodeText = JSON.stringify({ $schema: 'https://opencode.ai/config.json' }, null, 2)
|
|
42
|
+
let opencode
|
|
43
|
+
if (await fse.pathExists(opencodePath)) {
|
|
44
|
+
const errors = []
|
|
45
|
+
opencodeText = await fse.readFile(opencodePath, 'utf-8')
|
|
46
|
+
opencode = parseJsonc(opencodeText, errors)
|
|
47
|
+
if (errors.length > 0 || !opencode || typeof opencode !== 'object' || Array.isArray(opencode)) {
|
|
48
|
+
throw new Error('opencode.jsonc could not be parsed')
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
opencode = { $schema: 'https://opencode.ai/config.json' }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!opencode.mcp?.['agentmemory']) {
|
|
55
|
+
const agentmemory = {
|
|
56
|
+
type: 'local',
|
|
57
|
+
command: ['npx', '-y', '@agentmemory/mcp'],
|
|
58
|
+
env: {
|
|
59
|
+
AGENTMEMORY_URL: 'http://localhost:3111',
|
|
60
|
+
},
|
|
61
|
+
enabled: true,
|
|
62
|
+
timeout: 120000,
|
|
63
|
+
}
|
|
64
|
+
opencodeText = applyEdits(opencodeText, modify(opencodeText, ['mcp', 'agentmemory'], agentmemory, {
|
|
65
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
66
|
+
}))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await fse.writeFile(opencodePath, opencodeText, 'utf-8')
|
|
70
|
+
success('agentmemory MCP server configured in opencode.jsonc')
|
|
71
|
+
} catch (err) {
|
|
72
|
+
error(`Failed to configure agentmemory MCP: ${err.message}`)
|
|
73
|
+
return { optedIn: true, installed: false }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Add agentmemory skills to skills-lock.json for batch install
|
|
77
|
+
await addSkillToLock('agentmemory-agents', {
|
|
78
|
+
source: 'rohitg00/agentmemory',
|
|
79
|
+
sourceType: 'github',
|
|
80
|
+
skillPath: 'plugin/skills/agentmemory-agents/SKILL.md',
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
info('Memory server: run `agentmemory` to start on localhost:3111')
|
|
84
|
+
info('Real-time viewer: http://localhost:3113')
|
|
85
|
+
info('Health check: curl http://localhost:3111/agentmemory/health')
|
|
86
|
+
|
|
87
|
+
return { optedIn: true, installed: true }
|
|
88
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import { info, success } from '../../utils/exec.js'
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const GUARDRAILS_FRAGMENT_DIR = path.resolve(__dirname, '../../fragments/guardrails')
|
|
8
|
+
|
|
9
|
+
const MARKER_SECTIONS = {
|
|
10
|
+
rtk: 'RTK',
|
|
11
|
+
codegraph: 'CODEGRAPH',
|
|
12
|
+
memory: 'MEMORY',
|
|
13
|
+
simpleEnglish: 'SIMPLE-ENGLISH',
|
|
14
|
+
humanizer: 'HUMANIZER',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const OPTIONAL_TOOL_GUIDANCE = {
|
|
18
|
+
codegraph: 'Use `codegraph_explore` to refine relevant symbols and file-disjointness for the current wave.',
|
|
19
|
+
memory: 'Use Agentmemory MCP tools for cross-session context and task-result notes.',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function replaceMarkerSection(content, startMarker, endMarker, replacement) {
|
|
23
|
+
if (!content.includes(startMarker) || !content.includes(endMarker)) return content
|
|
24
|
+
const escape = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
25
|
+
const pattern = new RegExp(`${escape(startMarker)}[\\s\\S]*?${escape(endMarker)}`)
|
|
26
|
+
return content.replace(pattern, `${startMarker}\n${replacement}\n${endMarker}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function patchOptionalToolGuidance(destSkillsDir, selections) {
|
|
30
|
+
const files = []
|
|
31
|
+
|
|
32
|
+
async function collect(dir) {
|
|
33
|
+
for (const entry of await fse.readdir(dir, { withFileTypes: true })) {
|
|
34
|
+
const entryPath = path.join(dir, entry.name)
|
|
35
|
+
if (entry.isDirectory()) await collect(entryPath)
|
|
36
|
+
else if (entry.name.endsWith('.md')) files.push(entryPath)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!await fse.pathExists(destSkillsDir)) return 0
|
|
41
|
+
await collect(destSkillsDir)
|
|
42
|
+
|
|
43
|
+
let patched = 0
|
|
44
|
+
for (const filePath of files) {
|
|
45
|
+
let content = await fse.readFile(filePath, 'utf-8')
|
|
46
|
+
const original = content
|
|
47
|
+
for (const [key, guidance] of Object.entries(OPTIONAL_TOOL_GUIDANCE)) {
|
|
48
|
+
const suffix = key.toUpperCase()
|
|
49
|
+
content = replaceMarkerSection(
|
|
50
|
+
content,
|
|
51
|
+
`<!-- PC-OPTIMIZATION-${suffix}-START -->`,
|
|
52
|
+
`<!-- PC-OPTIMIZATION-${suffix}-END -->`,
|
|
53
|
+
selections[key] ? guidance : '',
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
if (content !== original) {
|
|
57
|
+
await fse.writeFile(filePath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
|
|
58
|
+
patched++
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return patched
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function patchGuardrails(selections = {}, { cwd = process.cwd() } = {}) {
|
|
65
|
+
const destSkillsDir = path.join(cwd, '.agents', 'skills')
|
|
66
|
+
const guardrailsPath = path.join(destSkillsDir, 'pc-guardrails-generic', 'SKILL.md')
|
|
67
|
+
let patchedCount = 0
|
|
68
|
+
|
|
69
|
+
if (await fse.pathExists(guardrailsPath)) {
|
|
70
|
+
let content = await fse.readFile(guardrailsPath, 'utf-8')
|
|
71
|
+
content = replaceMarkerSection(
|
|
72
|
+
content,
|
|
73
|
+
'<!-- PC-GUARDRAILS-CAVEMAN-START -->',
|
|
74
|
+
'<!-- PC-GUARDRAILS-CAVEMAN-END -->',
|
|
75
|
+
'',
|
|
76
|
+
).replace(
|
|
77
|
+
'<!-- PC-GUARDRAILS-CAVEMAN-START -->\n\n<!-- PC-GUARDRAILS-CAVEMAN-END -->',
|
|
78
|
+
'<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-START -->\n<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-END -->',
|
|
79
|
+
).replace(
|
|
80
|
+
'1. Load ALL skills listed under your own `## Abilities` now (Guardrails first, then the rest), by calling the `skill` tool once per `@skill-name`.',
|
|
81
|
+
'1. The `pc-system-reminders` plugin has already loaded the skills listed under your `## Abilities`, guardrails first.',
|
|
82
|
+
)
|
|
83
|
+
for (const [key, markerSuffix] of Object.entries(MARKER_SECTIONS)) {
|
|
84
|
+
let sectionContent = ''
|
|
85
|
+
if (selections[key]) {
|
|
86
|
+
const presetName = key === 'simpleEnglish' ? 'simple-english' : key
|
|
87
|
+
const presetPath = path.join(GUARDRAILS_FRAGMENT_DIR, `${presetName}.md`)
|
|
88
|
+
if (await fse.pathExists(presetPath)) {
|
|
89
|
+
sectionContent = (await fse.readFile(presetPath, 'utf-8')).trim()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
content = replaceMarkerSection(
|
|
93
|
+
content,
|
|
94
|
+
`<!-- PC-GUARDRAILS-${markerSuffix}-START -->`,
|
|
95
|
+
`<!-- PC-GUARDRAILS-${markerSuffix}-END -->`,
|
|
96
|
+
sectionContent,
|
|
97
|
+
)
|
|
98
|
+
if (selections[key]) patchedCount++
|
|
99
|
+
}
|
|
100
|
+
await fse.writeFile(guardrailsPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
|
|
101
|
+
} else {
|
|
102
|
+
info('pc-guardrails-generic not found, skipping guardrails patching')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const optionalPatched = await patchOptionalToolGuidance(destSkillsDir, selections)
|
|
106
|
+
success(`Guardrails patched: ${patchedCount} sections injected; ${optionalPatched} optional tool file(s) updated`)
|
|
107
|
+
return { patched: true, count: patchedCount, optionalPatched }
|
|
108
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser'
|
|
6
|
+
import { error, header, info, loading, success, warn } from '../../utils/exec.js'
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const QUOTA_PRESET_PATH = path.resolve(__dirname, '../../presets/quota.json')
|
|
10
|
+
const quotaPreset = await fse.readJson(QUOTA_PRESET_PATH)
|
|
11
|
+
const PLUGIN = quotaPreset.plugin
|
|
12
|
+
|
|
13
|
+
export function splitPackageSpec(spec) {
|
|
14
|
+
const separator = spec.startsWith('@') ? spec.indexOf('@', 1) : spec.lastIndexOf('@')
|
|
15
|
+
if (separator === -1) return { name: spec, version: 'latest' }
|
|
16
|
+
return { name: spec.slice(0, separator), version: spec.slice(separator + 1) }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ensurePlugin(config) {
|
|
20
|
+
if (!Array.isArray(config.plugin)) config.plugin = []
|
|
21
|
+
if (!config.plugin.includes(PLUGIN)) config.plugin.push(PLUGIN)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function installQuota(options = {}) {
|
|
25
|
+
if (!options.skipHeader) header('Installing opencode-quota')
|
|
26
|
+
|
|
27
|
+
let shouldInstall = true
|
|
28
|
+
if (!options.skipPrompt && process.stdin.isTTY) {
|
|
29
|
+
// Same abort-on-timeout pattern as tokenOptimizationStep: never race a
|
|
30
|
+
// live inquirer prompt, it leaks raw-mode stdin and unhandled rejections.
|
|
31
|
+
const controller = new AbortController()
|
|
32
|
+
const timer = setTimeout(() => controller.abort(), quotaPreset.prompt.timeoutMs)
|
|
33
|
+
try {
|
|
34
|
+
shouldInstall = await confirm(
|
|
35
|
+
{
|
|
36
|
+
message: quotaPreset.prompt.message,
|
|
37
|
+
default: quotaPreset.prompt.default,
|
|
38
|
+
},
|
|
39
|
+
{ signal: controller.signal },
|
|
40
|
+
)
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err.name !== 'AbortPromptError') throw err
|
|
43
|
+
shouldInstall = true
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timer)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!shouldInstall) {
|
|
50
|
+
warn('Skipped opencode-quota installation')
|
|
51
|
+
return { optedIn: false, installed: false }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
loading('configuring opencode-quota...')
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const opencodeDir = path.join(process.cwd(), '.opencode')
|
|
58
|
+
const opencodePath = path.join(process.cwd(), 'opencode.jsonc')
|
|
59
|
+
const pkgPath = path.join(opencodeDir, 'package.json')
|
|
60
|
+
const tuiPath = path.join(opencodeDir, 'tui.json')
|
|
61
|
+
const quotaDir = path.join(opencodeDir, 'opencode-quota')
|
|
62
|
+
const quotaPath = path.join(quotaDir, 'quota-toast.json')
|
|
63
|
+
|
|
64
|
+
let opencodeText = JSON.stringify({ $schema: 'https://opencode.ai/config.json' }, null, 2)
|
|
65
|
+
let opencode
|
|
66
|
+
if (await fse.pathExists(opencodePath)) {
|
|
67
|
+
const errors = []
|
|
68
|
+
opencodeText = await fse.readFile(opencodePath, 'utf-8')
|
|
69
|
+
opencode = parseJsonc(opencodeText, errors)
|
|
70
|
+
if (errors.length > 0 || !opencode || typeof opencode !== 'object' || Array.isArray(opencode)) {
|
|
71
|
+
throw new Error('opencode.jsonc could not be parsed')
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
opencode = { $schema: 'https://opencode.ai/config.json' }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const pkg = await fse.pathExists(pkgPath)
|
|
78
|
+
? await fse.readJson(pkgPath)
|
|
79
|
+
: {}
|
|
80
|
+
|
|
81
|
+
const tui = await fse.pathExists(tuiPath)
|
|
82
|
+
? await fse.readJson(tuiPath)
|
|
83
|
+
: { $schema: 'https://opencode.ai/tui.json' }
|
|
84
|
+
|
|
85
|
+
const plugins = Array.isArray(opencode.plugin) ? opencode.plugin : []
|
|
86
|
+
if (!plugins.includes(PLUGIN)) {
|
|
87
|
+
plugins.push(PLUGIN)
|
|
88
|
+
opencodeText = applyEdits(opencodeText, modify(opencodeText, ['plugin'], plugins, {
|
|
89
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
90
|
+
}))
|
|
91
|
+
}
|
|
92
|
+
ensurePlugin(tui)
|
|
93
|
+
|
|
94
|
+
if (!pkg.dependencies) pkg.dependencies = {}
|
|
95
|
+
const { name: pkgName, version: pkgVersion } = splitPackageSpec(PLUGIN)
|
|
96
|
+
if (pkg.dependencies[pkgName] !== pkgVersion) pkg.dependencies[pkgName] = pkgVersion
|
|
97
|
+
|
|
98
|
+
await fse.ensureDir(opencodeDir)
|
|
99
|
+
await fse.writeFile(opencodePath, opencodeText, 'utf-8')
|
|
100
|
+
await fse.writeJson(pkgPath, pkg, { spaces: 2 })
|
|
101
|
+
await fse.writeJson(tuiPath, tui, { spaces: 2 })
|
|
102
|
+
|
|
103
|
+
const quotaConfig = await fse.pathExists(quotaPath)
|
|
104
|
+
? await fse.readJson(quotaPath)
|
|
105
|
+
: {}
|
|
106
|
+
|
|
107
|
+
Object.assign(quotaConfig, quotaPreset.defaults)
|
|
108
|
+
|
|
109
|
+
await fse.ensureDir(quotaDir)
|
|
110
|
+
await fse.writeJson(quotaPath, quotaConfig, { spaces: 2 })
|
|
111
|
+
|
|
112
|
+
success('opencode-quota configured (manual setup)')
|
|
113
|
+
info('Restart OpenCode and run /quota to verify')
|
|
114
|
+
return { optedIn: true, installed: true }
|
|
115
|
+
} catch (err) {
|
|
116
|
+
error(`Failed to configure opencode-quota: ${err.message}`)
|
|
117
|
+
return { optedIn: true, installed: false }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { addSkillToLock } from './skills-lock.js'
|
|
2
|
+
import { header, success, info } from '../../utils/exec.js'
|
|
3
|
+
|
|
4
|
+
const SKILL_ENTRY = {
|
|
5
|
+
source: 'AminBlg/SimpleEnglish',
|
|
6
|
+
sourceType: 'github',
|
|
7
|
+
skillPath: 'skills/simple-english/SKILL.md',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function installSimpleEnglish(options = {}) {
|
|
11
|
+
if (!options.skipHeader) header('Installing Simple English')
|
|
12
|
+
|
|
13
|
+
info('Adding Simple English to skills-lock.json for batch install')
|
|
14
|
+
await addSkillToLock('simple-english', SKILL_ENTRY)
|
|
15
|
+
success('Simple English queued for installation')
|
|
16
|
+
return { optedIn: true, installed: true }
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { info, warn } from '../../utils/exec.js'
|
|
4
|
+
|
|
5
|
+
export async function addSkillToLock(skillName, entry) {
|
|
6
|
+
if (!entry) {
|
|
7
|
+
warn(`No entry provided for skills-lock.json: ${skillName}`)
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const destLock = path.join(process.cwd(), 'skills-lock.json')
|
|
12
|
+
|
|
13
|
+
let lock = { version: 1, skills: {} }
|
|
14
|
+
if (await fse.pathExists(destLock)) {
|
|
15
|
+
try {
|
|
16
|
+
lock = await fse.readJson(destLock)
|
|
17
|
+
if (!lock.skills) lock.skills = {}
|
|
18
|
+
} catch {
|
|
19
|
+
warn('Could not parse existing skills-lock.json, creating fresh')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!lock.skills[skillName]) {
|
|
24
|
+
lock.skills[skillName] = entry
|
|
25
|
+
await fse.writeJson(destLock, lock, { spaces: 2 })
|
|
26
|
+
info(`Added ${skillName} to skills-lock.json`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return true
|
|
30
|
+
}
|