@plainconceptsplatform/agent-harness 2.2.1 → 2.4.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 (155) hide show
  1. package/README.md +23 -12
  2. package/{src → cli}/commands/join.js +3 -4
  3. package/{src → cli}/commands/migrate.js +1 -1
  4. package/{src → cli}/commands/update.js +13 -1
  5. package/{src → cli}/commands/wizard.js +1 -1
  6. package/{src → cli}/index.js +1 -1
  7. package/{src → cli}/presets/agents-content.json +4 -4
  8. package/cli/presets/browser.json +10 -0
  9. package/{src → cli}/presets/clean.json +21 -21
  10. package/{src → cli}/presets/optimization.json +37 -37
  11. package/{src → cli}/presets/platforms.json +76 -76
  12. package/{src → cli}/presets/quota.json +16 -16
  13. package/{src → cli}/presets/source.json +23 -23
  14. package/cli/steps/browser/index.js +48 -0
  15. package/{src → cli}/steps/clean/index.js +120 -120
  16. package/{src → cli}/steps/copy/agents.js +119 -119
  17. package/{src → cli}/steps/copy/commands.js +95 -95
  18. package/{src → cli}/steps/copy/fullstack-engineer.js +5 -1
  19. package/{src → cli}/steps/copy/index.js +1 -1
  20. package/{src → cli}/steps/copy/opencode-json.js +50 -1
  21. package/{src → cli}/steps/copy/skills.js +2 -2
  22. package/{src → cli}/steps/metadata/index.js +4 -0
  23. package/{src → cli}/steps/models/format.js +88 -88
  24. package/{src → cli}/steps/models/index.js +64 -64
  25. package/{src → cli}/steps/openspec/index.js +136 -136
  26. package/{src → cli}/steps/optimization/codegraph.js +127 -127
  27. package/cli/steps/optimization/detect.js +66 -0
  28. package/{src → cli}/steps/optimization/humanizer.js +17 -17
  29. package/{src → cli}/steps/optimization/index.js +163 -163
  30. package/{src → cli}/steps/optimization/memory.js +88 -88
  31. package/{src → cli}/steps/optimization/patch-guardrails.js +109 -109
  32. package/{src → cli}/steps/optimization/quota.js +119 -119
  33. package/{src → cli}/steps/optimization/simple-english.js +17 -17
  34. package/{src → cli}/steps/optimization/skills-lock.js +30 -30
  35. package/{src → cli}/steps/platform/index.js +109 -109
  36. package/{src → cli}/steps/source/index.js +123 -123
  37. package/cli/utils/agent-color.js +111 -0
  38. package/{src → cli}/utils/exec-spinner.js +47 -47
  39. package/{src → cli}/utils/exec.js +134 -134
  40. package/{src → cli}/utils/models-pricing.js +42 -42
  41. package/{src → cli}/utils/paths.js +1 -1
  42. package/{src → cli}/utils/process.js +3 -3
  43. package/{src → cli}/utils/terminal.js +6 -6
  44. package/harness/.agents/skills/browser-automation/SKILL.md +72 -0
  45. package/{src/content → harness}/.agents/skills/pc-make-engineer/SKILL.md +1 -1
  46. package/{src/content → harness}/.agents/skills/pc-make-engineer/template.md +1 -2
  47. package/{src/content → harness}/.agents/skills/pc-plan-archive/SKILL.md +3 -0
  48. package/{src/content → harness}/.agents/skills/pc-plan-explore/SKILL.md +3 -0
  49. package/{src/content → harness}/.agents/skills/pc-plan-goal/SKILL.md +5 -16
  50. package/{src/content → harness}/.agents/skills/pc-plan-goal/branching.md +1 -1
  51. package/{src/content → harness}/.agents/skills/pc-plan-goal/output.md +5 -8
  52. package/{src/content → harness}/.agents/skills/pc-plan-story/SKILL.md +3 -0
  53. package/{src/content → harness}/.agents/skills/pc-userstory-browser/SKILL.md +136 -132
  54. package/{src/content → harness}/.opencode/package.json +9 -10
  55. package/{src/content → harness}/.opencode/plugins/pc-subagent-tiers.js +91 -4
  56. package/{src/content → harness}/ARCHITECTURE.md +16 -16
  57. package/{src/content → harness}/DESIGN.md +16 -16
  58. package/{src/content → harness}/opencode.jsonc +11 -3
  59. package/{src/content → harness}/openspec/config.yaml +20 -20
  60. package/{src/content → harness}/skills-lock.json +17 -17
  61. package/package.json +7 -7
  62. package/src/content/.agents/skills/browser-automation/SKILL.md +0 -66
  63. package/src/presets/browser.json +0 -22
  64. package/src/steps/browser/index.js +0 -91
  65. /package/{src → cli}/commands/shared.js +0 -0
  66. /package/{src → cli}/commands/single.js +0 -0
  67. /package/{src → cli}/fragments/archive/az.md +0 -0
  68. /package/{src → cli}/fragments/archive/gh.md +0 -0
  69. /package/{src → cli}/fragments/archive/gl.md +0 -0
  70. /package/{src → cli}/fragments/archive/none.md +0 -0
  71. /package/{src → cli}/fragments/guardrails/codegraph.md +0 -0
  72. /package/{src → cli}/fragments/guardrails/humanizer.md +0 -0
  73. /package/{src → cli}/fragments/guardrails/memory.md +0 -0
  74. /package/{src → cli}/fragments/guardrails/rtk.md +0 -0
  75. /package/{src → cli}/fragments/guardrails/simple-english.md +0 -0
  76. /package/{src → cli}/fragments/ops-backlog/az.md +0 -0
  77. /package/{src → cli}/fragments/ops-backlog/gh.md +0 -0
  78. /package/{src → cli}/fragments/ops-backlog/jira.md +0 -0
  79. /package/{src → cli}/fragments/ops-evidence/az.md +0 -0
  80. /package/{src → cli}/fragments/ops-evidence/gh.md +0 -0
  81. /package/{src → cli}/fragments/ops-evidence/jira.md +0 -0
  82. /package/{src → cli}/fragments/ops-review/az.md +0 -0
  83. /package/{src → cli}/fragments/ops-review/gh.md +0 -0
  84. /package/{src → cli}/fragments/ops-review/gl.md +0 -0
  85. /package/{src → cli}/fragments/ops-ship/az.md +0 -0
  86. /package/{src → cli}/fragments/ops-ship/gh.md +0 -0
  87. /package/{src → cli}/fragments/ops-ship/gl.md +0 -0
  88. /package/{src → cli}/presets/models.json +0 -0
  89. /package/{src → cli}/presets/openspec.json +0 -0
  90. /package/{src → cli}/steps/models/write.js +0 -0
  91. /package/{src → cli}/utils/copy.js +0 -0
  92. /package/{src → cli}/utils/legacy-check.js +0 -0
  93. /package/{src → cli}/utils/models-cache.js +0 -0
  94. /package/{src → cli}/utils/update-manifest.js +0 -0
  95. /package/{src/content → harness}/.agents/skills/pc-guardrails-generic/SKILL.md +0 -0
  96. /package/{src/content → harness}/.agents/skills/pc-guardrails-project/SKILL.md +0 -0
  97. /package/{src/content → harness}/.agents/skills/pc-make-architecture/SKILL.md +0 -0
  98. /package/{src/content → harness}/.agents/skills/pc-make-architecture/structure-template.md +0 -0
  99. /package/{src/content → harness}/.agents/skills/pc-make-design/SKILL.md +0 -0
  100. /package/{src/content → harness}/.agents/skills/pc-make-engineer/signal-mapping.md +0 -0
  101. /package/{src/content → harness}/.agents/skills/pc-make-evidence-scaffold/SKILL.md +0 -0
  102. /package/{src/content → harness}/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +0 -0
  103. /package/{src/content → harness}/.agents/skills/pc-make-guardrails/SKILL.md +0 -0
  104. /package/{src/content → harness}/.agents/skills/pc-make-guardrails/category-reference.md +0 -0
  105. /package/{src/content → harness}/.agents/skills/pc-make-merge-risk-assess/SKILL.md +0 -0
  106. /package/{src/content → harness}/.agents/skills/pc-make-merge-risk-assess/category-reference.md +0 -0
  107. /package/{src/content → harness}/.agents/skills/pc-make-user-model/SKILL.md +0 -0
  108. /package/{src/content → harness}/.agents/skills/pc-ops-evidence/SKILL.md +0 -0
  109. /package/{src/content → harness}/.agents/skills/pc-ops-ship/SKILL.md +0 -0
  110. /package/{src/content → harness}/.agents/skills/pc-plan-apply/SKILL.md +0 -0
  111. /package/{src/content → harness}/.agents/skills/pc-plan-apply/simple-mode.md +0 -0
  112. /package/{src/content → harness}/.agents/skills/pc-plan-goal/failure-policy.md +0 -0
  113. /package/{src/content → harness}/.agents/skills/pc-plan-goal/output-mode.md +0 -0
  114. /package/{src/content → harness}/.agents/skills/pc-plan-propose/SKILL.md +0 -0
  115. /package/{src/content → harness}/.agents/skills/pc-plan-propose/task-annotation.md +0 -0
  116. /package/{src/content → harness}/.agents/skills/pc-plan-quick/SKILL.md +0 -0
  117. /package/{src/content → harness}/.agents/skills/pc-repo-audit/SKILL.md +0 -0
  118. /package/{src/content → harness}/.agents/skills/pc-repo-help/SKILL.md +0 -0
  119. /package/{src/content → harness}/.agents/skills/pc-repo-initialize/SKILL.md +0 -0
  120. /package/{src/content → harness}/.agents/skills/pc-repo-onboard/SKILL.md +0 -0
  121. /package/{src/content → harness}/.agents/skills/pc-repo-verify/SKILL.md +0 -0
  122. /package/{src/content → harness}/.agents/skills/pc-userstory-az/SKILL.md +0 -0
  123. /package/{src/content → harness}/.agents/skills/pc-userstory-gh/SKILL.md +0 -0
  124. /package/{src/content → harness}/.agents/skills/pc-userstory-jira/SKILL.md +0 -0
  125. /package/{src/content → harness}/.opencode/_gitignore +0 -0
  126. /package/{src/content → harness}/.opencode/commands/init.md +0 -0
  127. /package/{src/content → harness}/.opencode/commands/make-architecture.md +0 -0
  128. /package/{src/content → harness}/.opencode/commands/make-design.md +0 -0
  129. /package/{src/content → harness}/.opencode/commands/make-engineer.md +0 -0
  130. /package/{src/content → harness}/.opencode/commands/make-evidence-scaffold.md +0 -0
  131. /package/{src/content → harness}/.opencode/commands/make-guardrails.md +0 -0
  132. /package/{src/content → harness}/.opencode/commands/make-user-model.md +0 -0
  133. /package/{src/content → harness}/.opencode/commands/ops-backlog.md +0 -0
  134. /package/{src/content → harness}/.opencode/commands/ops-evidence.md +0 -0
  135. /package/{src/content → harness}/.opencode/commands/ops-review.md +0 -0
  136. /package/{src/content → harness}/.opencode/commands/ops-ship.md +0 -0
  137. /package/{src/content → harness}/.opencode/commands/plan-apply.md +0 -0
  138. /package/{src/content → harness}/.opencode/commands/plan-archive.md +0 -0
  139. /package/{src/content → harness}/.opencode/commands/plan-explore.md +0 -0
  140. /package/{src/content → harness}/.opencode/commands/plan-goal.md +0 -0
  141. /package/{src/content → harness}/.opencode/commands/plan-propose.md +0 -0
  142. /package/{src/content → harness}/.opencode/commands/plan-quick.md +0 -0
  143. /package/{src/content → harness}/.opencode/commands/plan-story.md +0 -0
  144. /package/{src/content → harness}/.opencode/commands/repo-audit.md +0 -0
  145. /package/{src/content → harness}/.opencode/commands/repo-help.md +0 -0
  146. /package/{src/content → harness}/.opencode/commands/repo-initialize.md +0 -0
  147. /package/{src/content → harness}/.opencode/commands/repo-onboard.md +0 -0
  148. /package/{src/content → harness}/.opencode/commands/repo-verify.md +0 -0
  149. /package/{src/content → harness}/.opencode/plugins/pc-subagent-monitor.js +0 -0
  150. /package/{src/content → harness}/.opencode/plugins/pc-system-reminders.js +0 -0
  151. /package/{src/content → harness}/.opencode/tui/pc-subagents.tsx +0 -0
  152. /package/{src/content → harness}/.opencode/tui.json +0 -0
  153. /package/{src/content → harness}/AGENTS.md +0 -0
  154. /package/{src/content → harness}/openspec/changes/archive/.gitkeep +0 -0
  155. /package/{src/content → harness}/openspec/specs/.gitkeep +0 -0
@@ -1,109 +1,109 @@
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
- // Replacer function so $&, $' and $` in injected content stay literal.
27
- return content.replace(pattern, () => `${startMarker}\n${replacement}\n${endMarker}`)
28
- }
29
-
30
- async function patchOptionalToolGuidance(destSkillsDir, selections) {
31
- const files = []
32
-
33
- async function collect(dir) {
34
- for (const entry of await fse.readdir(dir, { withFileTypes: true })) {
35
- const entryPath = path.join(dir, entry.name)
36
- if (entry.isDirectory()) await collect(entryPath)
37
- else if (entry.name.endsWith('.md')) files.push(entryPath)
38
- }
39
- }
40
-
41
- if (!await fse.pathExists(destSkillsDir)) return 0
42
- await collect(destSkillsDir)
43
-
44
- let patched = 0
45
- for (const filePath of files) {
46
- let content = await fse.readFile(filePath, 'utf-8')
47
- const original = content
48
- for (const [key, guidance] of Object.entries(OPTIONAL_TOOL_GUIDANCE)) {
49
- const suffix = key.toUpperCase()
50
- content = replaceMarkerSection(
51
- content,
52
- `<!-- PC-OPTIMIZATION-${suffix}-START -->`,
53
- `<!-- PC-OPTIMIZATION-${suffix}-END -->`,
54
- selections[key] ? guidance : '',
55
- )
56
- }
57
- if (content !== original) {
58
- await fse.writeFile(filePath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
59
- patched++
60
- }
61
- }
62
- return patched
63
- }
64
-
65
- export async function patchGuardrails(selections = {}, { cwd = process.cwd() } = {}) {
66
- const destSkillsDir = path.join(cwd, '.agents', 'skills')
67
- const guardrailsPath = path.join(destSkillsDir, 'pc-guardrails-generic', 'SKILL.md')
68
- let patchedCount = 0
69
-
70
- if (await fse.pathExists(guardrailsPath)) {
71
- let content = await fse.readFile(guardrailsPath, 'utf-8')
72
- content = replaceMarkerSection(
73
- content,
74
- '<!-- PC-GUARDRAILS-CAVEMAN-START -->',
75
- '<!-- PC-GUARDRAILS-CAVEMAN-END -->',
76
- '',
77
- ).replace(
78
- '<!-- PC-GUARDRAILS-CAVEMAN-START -->\n\n<!-- PC-GUARDRAILS-CAVEMAN-END -->',
79
- '<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-START -->\n<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-END -->',
80
- ).replace(
81
- '1. Load ALL skills listed under your own `## Abilities` now (Guardrails first, then the rest), by calling the `skill` tool once per `@skill-name`.',
82
- '1. The `pc-system-reminders` plugin has already loaded the skills listed under your `## Abilities`, guardrails first.',
83
- )
84
- for (const [key, markerSuffix] of Object.entries(MARKER_SECTIONS)) {
85
- let sectionContent = ''
86
- if (selections[key]) {
87
- const presetName = key === 'simpleEnglish' ? 'simple-english' : key
88
- const presetPath = path.join(GUARDRAILS_FRAGMENT_DIR, `${presetName}.md`)
89
- if (await fse.pathExists(presetPath)) {
90
- sectionContent = (await fse.readFile(presetPath, 'utf-8')).trim()
91
- }
92
- }
93
- content = replaceMarkerSection(
94
- content,
95
- `<!-- PC-GUARDRAILS-${markerSuffix}-START -->`,
96
- `<!-- PC-GUARDRAILS-${markerSuffix}-END -->`,
97
- sectionContent,
98
- )
99
- if (selections[key]) patchedCount++
100
- }
101
- await fse.writeFile(guardrailsPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
102
- } else {
103
- info('pc-guardrails-generic not found, skipping guardrails patching')
104
- }
105
-
106
- const optionalPatched = await patchOptionalToolGuidance(destSkillsDir, selections)
107
- success(`Guardrails patched: ${patchedCount} sections injected; ${optionalPatched} optional tool file(s) updated`)
108
- return { patched: true, count: patchedCount, optionalPatched }
109
- }
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: 'Agentmemory carries context across sessions and agents: before exploring an unfamiliar area, call `memory_smart_search` with the key concepts of the task (components, error text) and reuse what it returns; when the work completes, call `memory_save` for durable non-obvious learnings (architecture constraints, root causes and fixes, decisions and why). Never store secrets.',
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
+ // Replacer function so $&, $' and $` in injected content stay literal.
27
+ return content.replace(pattern, () => `${startMarker}\n${replacement}\n${endMarker}`)
28
+ }
29
+
30
+ async function patchOptionalToolGuidance(destSkillsDir, selections) {
31
+ const files = []
32
+
33
+ async function collect(dir) {
34
+ for (const entry of await fse.readdir(dir, { withFileTypes: true })) {
35
+ const entryPath = path.join(dir, entry.name)
36
+ if (entry.isDirectory()) await collect(entryPath)
37
+ else if (entry.name.endsWith('.md')) files.push(entryPath)
38
+ }
39
+ }
40
+
41
+ if (!await fse.pathExists(destSkillsDir)) return 0
42
+ await collect(destSkillsDir)
43
+
44
+ let patched = 0
45
+ for (const filePath of files) {
46
+ let content = await fse.readFile(filePath, 'utf-8')
47
+ const original = content
48
+ for (const [key, guidance] of Object.entries(OPTIONAL_TOOL_GUIDANCE)) {
49
+ const suffix = key.toUpperCase()
50
+ content = replaceMarkerSection(
51
+ content,
52
+ `<!-- PC-OPTIMIZATION-${suffix}-START -->`,
53
+ `<!-- PC-OPTIMIZATION-${suffix}-END -->`,
54
+ selections[key] ? guidance : '',
55
+ )
56
+ }
57
+ if (content !== original) {
58
+ await fse.writeFile(filePath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
59
+ patched++
60
+ }
61
+ }
62
+ return patched
63
+ }
64
+
65
+ export async function patchGuardrails(selections = {}, { cwd = process.cwd() } = {}) {
66
+ const destSkillsDir = path.join(cwd, '.agents', 'skills')
67
+ const guardrailsPath = path.join(destSkillsDir, 'pc-guardrails-generic', 'SKILL.md')
68
+ let patchedCount = 0
69
+
70
+ if (await fse.pathExists(guardrailsPath)) {
71
+ let content = await fse.readFile(guardrailsPath, 'utf-8')
72
+ content = replaceMarkerSection(
73
+ content,
74
+ '<!-- PC-GUARDRAILS-CAVEMAN-START -->',
75
+ '<!-- PC-GUARDRAILS-CAVEMAN-END -->',
76
+ '',
77
+ ).replace(
78
+ '<!-- PC-GUARDRAILS-CAVEMAN-START -->\n\n<!-- PC-GUARDRAILS-CAVEMAN-END -->',
79
+ '<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-START -->\n<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-END -->',
80
+ ).replace(
81
+ '1. Load ALL skills listed under your own `## Abilities` now (Guardrails first, then the rest), by calling the `skill` tool once per `@skill-name`.',
82
+ '1. The `pc-system-reminders` plugin has already loaded the skills listed under your `## Abilities`, guardrails first.',
83
+ )
84
+ for (const [key, markerSuffix] of Object.entries(MARKER_SECTIONS)) {
85
+ let sectionContent = ''
86
+ if (selections[key]) {
87
+ const presetName = key === 'simpleEnglish' ? 'simple-english' : key
88
+ const presetPath = path.join(GUARDRAILS_FRAGMENT_DIR, `${presetName}.md`)
89
+ if (await fse.pathExists(presetPath)) {
90
+ sectionContent = (await fse.readFile(presetPath, 'utf-8')).trim()
91
+ }
92
+ }
93
+ content = replaceMarkerSection(
94
+ content,
95
+ `<!-- PC-GUARDRAILS-${markerSuffix}-START -->`,
96
+ `<!-- PC-GUARDRAILS-${markerSuffix}-END -->`,
97
+ sectionContent,
98
+ )
99
+ if (selections[key]) patchedCount++
100
+ }
101
+ await fse.writeFile(guardrailsPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
102
+ } else {
103
+ info('pc-guardrails-generic not found, skipping guardrails patching')
104
+ }
105
+
106
+ const optionalPatched = await patchOptionalToolGuidance(destSkillsDir, selections)
107
+ success(`Guardrails patched: ${patchedCount} sections injected; ${optionalPatched} optional tool file(s) updated`)
108
+ return { patched: true, count: patchedCount, optionalPatched }
109
+ }
@@ -1,119 +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
- }
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
+ }
@@ -1,17 +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
- }
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
+ }
@@ -1,30 +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
- }
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
+ }