helloagents 3.0.3-beta.1 → 3.0.8-beta.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +157 -57
- package/README_CN.md +157 -57
- package/bootstrap-lite.md +125 -50
- package/bootstrap.md +169 -123
- package/cli.mjs +80 -427
- package/gemini-extension.json +1 -1
- package/hooks/hooks-claude.json +10 -0
- package/hooks/hooks.json +10 -0
- package/package.json +1 -1
- package/scripts/advisor-state.mjs +222 -0
- package/scripts/capability-registry.mjs +59 -0
- package/scripts/cli-codex-backup.mjs +59 -0
- package/scripts/cli-codex-config.mjs +94 -0
- package/scripts/cli-codex.mjs +90 -222
- package/scripts/cli-config.mjs +1 -0
- package/scripts/cli-doctor-render.mjs +28 -0
- package/scripts/cli-doctor.mjs +370 -0
- package/scripts/cli-host-detect.mjs +94 -0
- package/scripts/cli-lifecycle-hosts.mjs +123 -0
- package/scripts/cli-lifecycle.mjs +213 -0
- package/scripts/cli-messages.mjs +76 -52
- package/scripts/cli-toml.mjs +30 -0
- package/scripts/closeout-state.mjs +213 -0
- package/scripts/delivery-gate.mjs +256 -0
- package/scripts/guard-rules.mjs +147 -0
- package/scripts/guard.mjs +218 -168
- package/scripts/notify-context.mjs +78 -23
- package/scripts/notify-events.mjs +5 -1
- package/scripts/notify-route.mjs +111 -0
- package/scripts/notify-shared.mjs +0 -2
- package/scripts/notify-source.mjs +113 -0
- package/scripts/notify-ui.mjs +40 -6
- package/scripts/notify.mjs +137 -65
- package/scripts/plan-contract.mjs +210 -0
- package/scripts/project-storage.mjs +235 -0
- package/scripts/ralph-loop.mjs +9 -58
- package/scripts/replay-state.mjs +210 -0
- package/scripts/review-state.mjs +220 -0
- package/scripts/runtime-context.mjs +74 -0
- package/scripts/turn-state.mjs +173 -0
- package/scripts/verify-state.mjs +226 -0
- package/scripts/visual-state.mjs +244 -0
- package/scripts/workflow-core.mjs +165 -0
- package/scripts/workflow-plan-files.mjs +249 -0
- package/scripts/workflow-recommendation.mjs +335 -0
- package/scripts/workflow-state.mjs +113 -0
- package/skills/_meta/SKILL.md +1 -1
- package/skills/commands/auto/SKILL.md +48 -67
- package/skills/commands/build/SKILL.md +67 -0
- package/skills/commands/clean/SKILL.md +10 -8
- package/skills/commands/commit/SKILL.md +8 -4
- package/skills/commands/help/SKILL.md +18 -11
- package/skills/commands/idea/SKILL.md +55 -0
- package/skills/commands/init/SKILL.md +16 -8
- package/skills/commands/loop/SKILL.md +6 -5
- package/skills/commands/plan/SKILL.md +118 -0
- package/skills/commands/prd/SKILL.md +22 -15
- package/skills/commands/verify/SKILL.md +32 -9
- package/skills/commands/wiki/SKILL.md +11 -11
- package/skills/hello-review/SKILL.md +9 -0
- package/skills/hello-subagent/SKILL.md +5 -3
- package/skills/hello-ui/SKILL.md +36 -8
- package/skills/hello-verify/SKILL.md +12 -3
- package/skills/helloagents/SKILL.md +36 -20
- package/templates/DESIGN.md +25 -4
- package/templates/STATE.md +3 -0
- package/templates/plans/contract.json +48 -0
- package/templates/plans/plan.md +23 -0
- package/templates/plans/tasks.md +3 -3
- package/skills/commands/design/SKILL.md +0 -108
- package/skills/commands/review/SKILL.md +0 -16
- package/templates/plans/design.md +0 -14
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { appendReplayEvent } from './replay-state.mjs'
|
|
6
|
+
import { captureWorkspaceFingerprint } from './verify-state.mjs'
|
|
7
|
+
|
|
8
|
+
export const ADVISOR_EVIDENCE_FILE_NAME = '.ralph-advisor.json'
|
|
9
|
+
const ADVISOR_EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
|
|
10
|
+
const VALID_ADVISOR_OUTCOMES = new Set(['clean', 'findings'])
|
|
11
|
+
const VALID_SOURCES = new Set(['claude', 'codex', 'gemini'])
|
|
12
|
+
|
|
13
|
+
function normalizeStringArray(values) {
|
|
14
|
+
if (!Array.isArray(values)) return []
|
|
15
|
+
return [...new Set(values.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean))]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeSources(values) {
|
|
19
|
+
return normalizeStringArray(values).filter((value) => VALID_SOURCES.has(value))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeOutcome(value) {
|
|
23
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
24
|
+
return VALID_ADVISOR_OUTCOMES.has(normalized) ? normalized : ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getAdvisorEvidencePath(cwd) {
|
|
28
|
+
return join(cwd, '.helloagents', ADVISOR_EVIDENCE_FILE_NAME)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readAdvisorEvidence(cwd) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(readFileSync(getAdvisorEvidencePath(cwd), 'utf-8'))
|
|
34
|
+
} catch {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function clearAdvisorEvidence(cwd) {
|
|
40
|
+
rmSync(getAdvisorEvidencePath(cwd), { force: true })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeAdvisorEvidence(input = {}) {
|
|
44
|
+
return {
|
|
45
|
+
source: typeof input.source === 'string' && input.source.trim() ? input.source.trim() : 'manual',
|
|
46
|
+
originCommand: typeof input.originCommand === 'string' ? input.originCommand.trim() : '',
|
|
47
|
+
reason: typeof input.reason === 'string' ? input.reason.trim() : '',
|
|
48
|
+
focus: normalizeStringArray(input.focus),
|
|
49
|
+
preferredSources: normalizeSources(input.preferredSources),
|
|
50
|
+
consultedSources: normalizeSources(input.consultedSources),
|
|
51
|
+
outcome: normalizeOutcome(input.outcome),
|
|
52
|
+
summary: typeof input.summary === 'string' ? input.summary.trim() : '',
|
|
53
|
+
findings: normalizeStringArray(input.findings),
|
|
54
|
+
recommendations: normalizeStringArray(input.recommendations),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function writeAdvisorEvidence(cwd, input = {}) {
|
|
59
|
+
mkdirSync(join(cwd, '.helloagents'), { recursive: true })
|
|
60
|
+
const normalized = normalizeAdvisorEvidence(input)
|
|
61
|
+
const payload = {
|
|
62
|
+
updatedAt: new Date().toISOString(),
|
|
63
|
+
...normalized,
|
|
64
|
+
fingerprint: captureWorkspaceFingerprint(cwd),
|
|
65
|
+
}
|
|
66
|
+
writeFileSync(getAdvisorEvidencePath(cwd), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
|
|
67
|
+
appendReplayEvent(cwd, {
|
|
68
|
+
event: 'advisor_evidence_written',
|
|
69
|
+
source: normalized.source,
|
|
70
|
+
skillName: normalized.originCommand,
|
|
71
|
+
details: {
|
|
72
|
+
reason: normalized.reason,
|
|
73
|
+
focus: normalized.focus,
|
|
74
|
+
preferredSources: normalized.preferredSources,
|
|
75
|
+
consultedSources: normalized.consultedSources,
|
|
76
|
+
outcome: normalized.outcome,
|
|
77
|
+
},
|
|
78
|
+
artifacts: ['.helloagents/.ralph-advisor.json'],
|
|
79
|
+
})
|
|
80
|
+
return payload
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readRequiredAdvisorEvidence(cwd, required) {
|
|
84
|
+
if (!required) {
|
|
85
|
+
return {
|
|
86
|
+
required: false,
|
|
87
|
+
status: 'not-applicable',
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const evidence = readAdvisorEvidence(cwd)
|
|
92
|
+
if (evidence) return { evidence }
|
|
93
|
+
return {
|
|
94
|
+
error: {
|
|
95
|
+
required: true,
|
|
96
|
+
status: 'missing',
|
|
97
|
+
details: ['missing advisor evidence required by the active contract'],
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateAdvisorTimestamp(evidence, now) {
|
|
103
|
+
const updatedAt = Date.parse(evidence.updatedAt || '')
|
|
104
|
+
if (!Number.isFinite(updatedAt)) {
|
|
105
|
+
return {
|
|
106
|
+
required: true,
|
|
107
|
+
status: 'invalid',
|
|
108
|
+
evidence,
|
|
109
|
+
details: ['advisor evidence timestamp is invalid'],
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (now - updatedAt > ADVISOR_EVIDENCE_MAX_AGE_MS) {
|
|
113
|
+
return {
|
|
114
|
+
required: true,
|
|
115
|
+
status: 'stale-time',
|
|
116
|
+
evidence,
|
|
117
|
+
details: ['advisor evidence is older than 30 minutes'],
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function validateAdvisorFingerprint(cwd, evidence) {
|
|
124
|
+
const currentFingerprint = captureWorkspaceFingerprint(cwd)
|
|
125
|
+
if (
|
|
126
|
+
currentFingerprint.available
|
|
127
|
+
&& evidence.fingerprint?.available
|
|
128
|
+
&& currentFingerprint.combined !== evidence.fingerprint.combined
|
|
129
|
+
) {
|
|
130
|
+
return {
|
|
131
|
+
required: true,
|
|
132
|
+
status: 'stale-diff',
|
|
133
|
+
evidence,
|
|
134
|
+
details: ['workspace diff changed after the last advisor evidence'],
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function validateAdvisorContent(evidence, focus = []) {
|
|
141
|
+
if (!normalizeOutcome(evidence.outcome) || !String(evidence.summary || '').trim() || !String(evidence.reason || '').trim()) {
|
|
142
|
+
return {
|
|
143
|
+
required: true,
|
|
144
|
+
status: 'invalid',
|
|
145
|
+
evidence,
|
|
146
|
+
details: ['advisor evidence must record explicit outcome, reason, and summary'],
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (normalizeSources(evidence.consultedSources).length === 0) {
|
|
150
|
+
return {
|
|
151
|
+
required: true,
|
|
152
|
+
status: 'invalid',
|
|
153
|
+
evidence,
|
|
154
|
+
details: ['advisor evidence must record at least one consulted source'],
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (normalizeStringArray(focus).length > 0 && normalizeStringArray(evidence.focus).length === 0) {
|
|
158
|
+
return {
|
|
159
|
+
required: true,
|
|
160
|
+
status: 'invalid',
|
|
161
|
+
evidence,
|
|
162
|
+
details: ['advisor evidence must retain the requested advisor focus'],
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (normalizeOutcome(evidence.outcome) !== 'clean') {
|
|
166
|
+
return {
|
|
167
|
+
required: true,
|
|
168
|
+
status: 'blocked',
|
|
169
|
+
evidence,
|
|
170
|
+
details: ['latest advisor evidence still records blocking findings'],
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getAdvisorEvidenceStatus(cwd, { required = false, focus = [], now = Date.now() } = {}) {
|
|
177
|
+
const requiredEvidence = readRequiredAdvisorEvidence(cwd, required)
|
|
178
|
+
if ('status' in requiredEvidence) return requiredEvidence
|
|
179
|
+
if (requiredEvidence.error) return requiredEvidence.error
|
|
180
|
+
|
|
181
|
+
const { evidence } = requiredEvidence
|
|
182
|
+
const timestampError = validateAdvisorTimestamp(evidence, now)
|
|
183
|
+
if (timestampError) return timestampError
|
|
184
|
+
|
|
185
|
+
const fingerprintError = validateAdvisorFingerprint(cwd, evidence)
|
|
186
|
+
if (fingerprintError) return fingerprintError
|
|
187
|
+
|
|
188
|
+
const contentError = validateAdvisorContent(evidence, focus)
|
|
189
|
+
if (contentError) return contentError
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
required: true,
|
|
193
|
+
status: 'valid',
|
|
194
|
+
evidence,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function readStdinJson() {
|
|
199
|
+
try {
|
|
200
|
+
return JSON.parse(readFileSync(0, 'utf-8'))
|
|
201
|
+
} catch {
|
|
202
|
+
return {}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function main() {
|
|
207
|
+
const command = process.argv[2] || ''
|
|
208
|
+
if (command !== 'write') return
|
|
209
|
+
|
|
210
|
+
const input = readStdinJson()
|
|
211
|
+
const cwd = input.cwd || process.cwd()
|
|
212
|
+
const payload = writeAdvisorEvidence(cwd, input)
|
|
213
|
+
process.stdout.write(JSON.stringify({
|
|
214
|
+
suppressOutput: true,
|
|
215
|
+
path: getAdvisorEvidencePath(cwd),
|
|
216
|
+
payload,
|
|
217
|
+
}))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
221
|
+
main()
|
|
222
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
import { getAdvisorRequirement, getVisualValidationRequirement } from './plan-contract.mjs'
|
|
4
|
+
import { describeProjectStoreFile, getProjectDesignContractPath } from './project-storage.mjs'
|
|
5
|
+
import { getWorkflowRecommendation, getWorkflowSnapshot } from './workflow-state.mjs'
|
|
6
|
+
|
|
7
|
+
function getPrimaryPlan(snapshot) {
|
|
8
|
+
return snapshot.activePlans[0] || snapshot.plans[0] || null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function selectCapabilities({ cwd, skillName = '' }) {
|
|
12
|
+
const snapshot = getWorkflowSnapshot(cwd)
|
|
13
|
+
const recommendation = getWorkflowRecommendation(cwd)
|
|
14
|
+
const plan = getPrimaryPlan(snapshot)
|
|
15
|
+
const advisorRequirement = getAdvisorRequirement(plan?.contract)
|
|
16
|
+
const visualRequirement = getVisualValidationRequirement(plan?.contract)
|
|
17
|
+
const capabilities = []
|
|
18
|
+
|
|
19
|
+
if (skillName === 'plan' || skillName === 'prd' || recommendation?.nextCommand === 'plan') {
|
|
20
|
+
capabilities.push({
|
|
21
|
+
id: 'plan-contract',
|
|
22
|
+
description: '结构化契约:仅在规划/PRD 场景使用 `scripts/plan-contract.mjs write` 写 `contract.json`,不要只把验证路径留在自然语言说明里。',
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
if (advisorRequirement.required) {
|
|
26
|
+
capabilities.push({
|
|
27
|
+
id: 'advisor-artifact',
|
|
28
|
+
description: advisorRequirement.styleRequired
|
|
29
|
+
? '风格 advisor:当前 UI 契约要求进入收尾前复查设计方向,并复用 `.helloagents/.ralph-advisor.json` 记录 reason、focus、consultedSources 与结论。'
|
|
30
|
+
: '独立 advisor:当前契约要求进入收尾前写 `.helloagents/.ralph-advisor.json`,记录 advisor reason、focus、consultedSources 与结论。',
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
if (plan?.contract?.verifyMode === 'review-first') {
|
|
34
|
+
capabilities.push({
|
|
35
|
+
id: 'review-evaluator',
|
|
36
|
+
description: '审查优先:当前验证主路径是 review-first,先做 hello-review,再做 hello-verify。',
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
if (plan?.contract?.ui?.required || existsSync(getProjectDesignContractPath(cwd))) {
|
|
40
|
+
capabilities.push({
|
|
41
|
+
id: 'design-contract',
|
|
42
|
+
description: `UI 契约:仅在 UI 场景按需读取当前 plan.md / prd/03-ui-design.md、${describeProjectStoreFile(cwd, 'DESIGN.md')} 与 hello-ui,不全局常驻。`,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
if (visualRequirement.required) {
|
|
46
|
+
capabilities.push({
|
|
47
|
+
id: 'visual-evaluator',
|
|
48
|
+
description: '视觉验收:当前 UI 契约要求进入收尾前写 `.helloagents/.ralph-visual.json`,记录 tooling、screensChecked、statesChecked、status 与 summary。',
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return capabilities
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildCapabilityHint({ cwd, skillName = '' }) {
|
|
56
|
+
const capabilities = selectCapabilities({ cwd, skillName })
|
|
57
|
+
if (capabilities.length === 0) return ''
|
|
58
|
+
return `按需能力:${capabilities.map((entry) => `${entry.id}=${entry.description}`).join(' ')}`
|
|
59
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { removeIfExists, safeRead } from './cli-utils.mjs'
|
|
5
|
+
|
|
6
|
+
const CODEX_BACKUP_TIMESTAMP_RE = /^\d{8}-\d{6}$/
|
|
7
|
+
|
|
8
|
+
function formatBackupTimestamp(date = new Date()) {
|
|
9
|
+
const pad = (value, size = 2) => String(value).padStart(size, '0')
|
|
10
|
+
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createTimestampedBackupPath(filePath, backupBaseName) {
|
|
14
|
+
return join(dirname(filePath), `${backupBaseName}_${formatBackupTimestamp()}.bak`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function listTimestampedBackups(directory, backupBaseName) {
|
|
18
|
+
if (!existsSync(directory)) return []
|
|
19
|
+
return readdirSync(directory)
|
|
20
|
+
.filter((name) => name.startsWith(`${backupBaseName}_`) && name.endsWith('.bak'))
|
|
21
|
+
.filter((name) => CODEX_BACKUP_TIMESTAMP_RE.test(name.slice(backupBaseName.length + 1, -4)))
|
|
22
|
+
.sort()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getLatestTimestampedBackupPath(filePath, backupBaseName) {
|
|
26
|
+
const backups = listTimestampedBackups(dirname(filePath), backupBaseName)
|
|
27
|
+
const latest = backups.at(-1)
|
|
28
|
+
return latest ? join(dirname(filePath), latest) : ''
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readLatestTimestampedBackup(filePath, backupBaseName) {
|
|
32
|
+
const backupPath = getLatestTimestampedBackupPath(filePath, backupBaseName)
|
|
33
|
+
return backupPath ? safeRead(backupPath) || '' : ''
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function removeLatestTimestampedBackup(filePath, backupBaseName) {
|
|
37
|
+
const backupPath = getLatestTimestampedBackupPath(filePath, backupBaseName)
|
|
38
|
+
if (backupPath) removeIfExists(backupPath)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ensureTimestampedBackup(filePath, backupBaseName) {
|
|
42
|
+
if (!existsSync(filePath)) return ''
|
|
43
|
+
const existingBackup = getLatestTimestampedBackupPath(filePath, backupBaseName)
|
|
44
|
+
if (existingBackup) return existingBackup
|
|
45
|
+
const backupPath = createTimestampedBackupPath(filePath, backupBaseName)
|
|
46
|
+
copyFileSync(filePath, backupPath)
|
|
47
|
+
return backupPath
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readCodexBackup(filePath, backupBaseName) {
|
|
51
|
+
const latest = readLatestTimestampedBackup(filePath, backupBaseName)
|
|
52
|
+
if (latest) return latest
|
|
53
|
+
return safeRead(`${filePath}.bak`) || ''
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function removeCodexBackup(filePath, backupBaseName) {
|
|
57
|
+
removeLatestTimestampedBackup(filePath, backupBaseName)
|
|
58
|
+
removeIfExists(`${filePath}.bak`)
|
|
59
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
prependTopLevelTomlBlocks,
|
|
3
|
+
removeTopLevelTomlBlock,
|
|
4
|
+
stripTomlSection,
|
|
5
|
+
} from './cli-toml.mjs'
|
|
6
|
+
|
|
7
|
+
export const CODEX_PLUGIN_CONFIG_HEADER = '[plugins."helloagents@local-plugins"]'
|
|
8
|
+
export const CODEX_MANAGED_TOML_COMMENT = '# helloagents-managed'
|
|
9
|
+
|
|
10
|
+
function normalizePath(value = '') {
|
|
11
|
+
return String(value || '').replace(/\\/g, '/')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function upsertCodexPluginConfig(text) {
|
|
15
|
+
const stripped = stripTomlSection(text, CODEX_PLUGIN_CONFIG_HEADER).text.trimEnd()
|
|
16
|
+
const block = `${CODEX_PLUGIN_CONFIG_HEADER}\nenabled = true`
|
|
17
|
+
return stripped ? `${stripped}\n\n${block}\n` : `${block}\n`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function removeCodexPluginConfig(text) {
|
|
21
|
+
return stripTomlSection(text, CODEX_PLUGIN_CONFIG_HEADER).text
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isManagedCodexModelInstruction(line = '') {
|
|
25
|
+
return line.includes('model_instructions_file')
|
|
26
|
+
&& line.includes(CODEX_MANAGED_TOML_COMMENT)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isManagedCodexNotify(line = '') {
|
|
30
|
+
return line.includes('codex-notify')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isManagedCodexBackupInstruction(line = '') {
|
|
34
|
+
return line.includes(CODEX_MANAGED_TOML_COMMENT)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isManagedCodexHooks(line = '') {
|
|
38
|
+
return /^\s*codex_hooks\s*=\s*true(?:\s+#.*)?\s*$/i.test(String(line || ''))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatManagedCodexModelInstructionsValue(filePath) {
|
|
42
|
+
return `"${normalizePath(filePath)}" ${CODEX_MANAGED_TOML_COMMENT}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatManagedCodexModelInstructionsLine(filePath) {
|
|
46
|
+
return `model_instructions_file = ${formatManagedCodexModelInstructionsValue(filePath)}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatManagedCodexNotifyValue(notifyScriptPath) {
|
|
50
|
+
return `["node", "${normalizePath(notifyScriptPath)}", "codex-notify"]`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatManagedCodexNotifyLine(notifyScriptPath) {
|
|
54
|
+
return `notify = ${formatManagedCodexNotifyValue(notifyScriptPath)}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function removeTopLevelLinesBeingReplaced(toml, lines) {
|
|
58
|
+
let next = toml
|
|
59
|
+
|
|
60
|
+
for (const line of lines.map((value) => String(value || '').trim()).filter(Boolean)) {
|
|
61
|
+
const key = line.slice(0, line.indexOf('=')).trim()
|
|
62
|
+
if (!key) continue
|
|
63
|
+
next = removeTopLevelTomlBlock(next, key)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return next
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function upsertOrderedCodexTopLevelLines(toml, lines) {
|
|
70
|
+
return prependTopLevelTomlBlocks(
|
|
71
|
+
removeTopLevelLinesBeingReplaced(toml, lines),
|
|
72
|
+
lines,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function installCodexModelInstructions(toml, filePath) {
|
|
77
|
+
return upsertOrderedCodexTopLevelLines(toml, [
|
|
78
|
+
formatManagedCodexModelInstructionsLine(filePath),
|
|
79
|
+
])
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function installCodexManagedTopLevelConfig(toml, { modelInstructionsPath, notifyScriptPath }) {
|
|
83
|
+
return upsertOrderedCodexTopLevelLines(toml, [
|
|
84
|
+
formatManagedCodexModelInstructionsLine(modelInstructionsPath),
|
|
85
|
+
formatManagedCodexNotifyLine(notifyScriptPath),
|
|
86
|
+
])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function restoreCodexTopLevelConfig(toml, { modelInstructionsLine = '', notifyLine = '' }) {
|
|
90
|
+
return upsertOrderedCodexTopLevelLines(toml, [
|
|
91
|
+
modelInstructionsLine,
|
|
92
|
+
notifyLine,
|
|
93
|
+
])
|
|
94
|
+
}
|