helloagents 3.0.12 → 3.0.16-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 +6 -4
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +182 -35
- package/README_CN.md +184 -37
- package/bootstrap-lite.md +32 -26
- package/bootstrap.md +35 -29
- package/cli.mjs +119 -11
- package/gemini-extension.json +1 -1
- package/install.ps1 +128 -0
- package/install.sh +121 -0
- package/package.json +23 -4
- package/scripts/advisor-state.mjs +36 -63
- package/scripts/capability-registry.mjs +4 -4
- package/scripts/cli-branch.mjs +84 -0
- package/scripts/cli-codex-config.mjs +14 -20
- package/scripts/cli-codex.mjs +32 -38
- package/scripts/cli-doctor-render.mjs +4 -0
- package/scripts/cli-doctor.mjs +40 -30
- package/scripts/cli-host-detect.mjs +0 -1
- package/scripts/cli-hosts.mjs +16 -8
- package/scripts/cli-lifecycle-hosts.mjs +119 -32
- package/scripts/cli-lifecycle.mjs +24 -13
- package/scripts/cli-messages.mjs +34 -16
- package/scripts/cli-runtime-carrier.mjs +15 -0
- package/scripts/cli-runtime-root.mjs +72 -0
- package/scripts/cli-toml.mjs +0 -79
- package/scripts/cli-utils.mjs +30 -4
- package/scripts/closeout-state.mjs +35 -62
- package/scripts/delivery-gate-messages.mjs +70 -0
- package/scripts/delivery-gate.mjs +9 -75
- package/scripts/guard-rules.mjs +42 -42
- package/scripts/guard.mjs +44 -24
- package/scripts/notify-context.mjs +19 -28
- package/scripts/notify-events.mjs +3 -1
- package/scripts/notify-gates.mjs +2 -0
- package/scripts/notify-route.mjs +9 -7
- package/scripts/notify-ui.mjs +42 -32
- package/scripts/notify.mjs +72 -36
- package/scripts/project-storage.mjs +35 -66
- package/scripts/ralph-loop.mjs +36 -31
- package/scripts/replay-state.mjs +31 -128
- package/scripts/review-state.mjs +34 -61
- package/scripts/runtime-artifacts.mjs +95 -0
- package/scripts/runtime-context.mjs +35 -29
- package/scripts/runtime-scope.mjs +313 -0
- package/scripts/session-capsule.mjs +202 -0
- package/scripts/turn-state-cli.mjs +17 -0
- package/scripts/turn-state.mjs +185 -66
- package/scripts/turn-stop-gate.mjs +24 -6
- package/scripts/verify-state.mjs +34 -85
- package/scripts/visual-state.mjs +38 -65
- package/scripts/workflow-core.mjs +3 -3
- package/scripts/workflow-plan-files.mjs +1 -1
- package/scripts/workflow-recommendation.mjs +17 -13
- package/scripts/workflow-state.mjs +5 -5
- package/skills/commands/build/SKILL.md +1 -1
- package/skills/commands/commit/SKILL.md +1 -1
- package/skills/commands/help/SKILL.md +5 -3
- package/skills/commands/loop/SKILL.md +1 -1
- package/skills/commands/plan/SKILL.md +8 -6
- package/skills/commands/prd/SKILL.md +5 -3
- package/skills/commands/verify/SKILL.md +5 -5
- package/skills/hello-debug/SKILL.md +20 -3
- package/skills/hello-review/SKILL.md +2 -2
- package/skills/hello-subagent/SKILL.md +2 -2
- package/skills/hello-test/SKILL.md +6 -2
- package/skills/hello-ui/SKILL.md +7 -7
- package/skills/hello-verify/SKILL.md +10 -7
- package/skills/helloagents/SKILL.md +14 -9
- package/templates/context.md +6 -0
- package/templates/plans/plan.md +3 -0
- package/templates/plans/tasks.md +8 -3
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { dirname, join, normalize, resolve } from 'node:path'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
|
|
7
|
+
import { resolveSessionToken } from './session-token.mjs'
|
|
8
|
+
|
|
9
|
+
export const PROJECT_DIR_NAME = '.helloagents'
|
|
10
|
+
export const PROJECT_SESSIONS_DIR_NAME = 'sessions'
|
|
11
|
+
export const PROJECT_ARTIFACTS_DIR_NAME = 'artifacts'
|
|
12
|
+
export const CAPSULE_FILE_NAME = 'capsule.json'
|
|
13
|
+
export const EVENTS_FILE_NAME = 'events.jsonl'
|
|
14
|
+
export const DEFAULT_STATE_SESSION_TOKEN = 'default'
|
|
15
|
+
export const USER_RUNTIME_DIR_NAME = 'runtime'
|
|
16
|
+
export const USER_RUNTIME_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000
|
|
17
|
+
|
|
18
|
+
function normalizePath(filePath = '') {
|
|
19
|
+
return filePath ? normalize(resolve(filePath)) : ''
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function runGit(cwd, args = []) {
|
|
23
|
+
try {
|
|
24
|
+
return execFileSync('git', args, {
|
|
25
|
+
cwd,
|
|
26
|
+
encoding: 'utf-8',
|
|
27
|
+
timeout: 5_000,
|
|
28
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
29
|
+
}).trim()
|
|
30
|
+
} catch {
|
|
31
|
+
return ''
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getHomeDir(env = process.env) {
|
|
36
|
+
return env.HOME || env.USERPROFILE || homedir()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeComparablePath(filePath = '') {
|
|
40
|
+
const resolved = normalizePath(filePath)
|
|
41
|
+
try {
|
|
42
|
+
return realpathSync.native(resolved)
|
|
43
|
+
} catch {
|
|
44
|
+
return resolved
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function samePath(left, right) {
|
|
49
|
+
const a = normalizeComparablePath(left)
|
|
50
|
+
const b = normalizeComparablePath(right)
|
|
51
|
+
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveGitTopLevel(cwd) {
|
|
55
|
+
const absolute = runGit(cwd, ['rev-parse', '--path-format=absolute', '--show-toplevel'])
|
|
56
|
+
if (absolute) return normalize(resolve(absolute))
|
|
57
|
+
|
|
58
|
+
const raw = runGit(cwd, ['rev-parse', '--show-toplevel'])
|
|
59
|
+
return raw ? normalize(resolve(cwd, raw)) : ''
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveGitBranchName(cwd) {
|
|
63
|
+
const branchName = runGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
64
|
+
if (branchName && branchName !== 'HEAD') return branchName
|
|
65
|
+
|
|
66
|
+
const symbolicName = runGit(cwd, ['symbolic-ref', '--quiet', '--short', 'HEAD'])
|
|
67
|
+
return symbolicName && symbolicName !== 'HEAD' ? symbolicName : ''
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function sanitizeRuntimeSegment(value = '', fallback = '') {
|
|
71
|
+
const normalized = String(value)
|
|
72
|
+
.trim()
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
75
|
+
.replace(/^-+|-+$/g, '')
|
|
76
|
+
.slice(0, 48)
|
|
77
|
+
return normalized || fallback
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function normalizeRuntimeOptions(options = {}) {
|
|
81
|
+
if (!options || typeof options !== 'object') return {}
|
|
82
|
+
if (options.payload && typeof options.payload === 'object') return options
|
|
83
|
+
return {
|
|
84
|
+
...options,
|
|
85
|
+
payload: options,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getProjectActivationDir(cwd) {
|
|
90
|
+
const activeDir = findProjectActivationDir(cwd)
|
|
91
|
+
return activeDir || join(normalizePath(cwd || process.cwd()), PROJECT_DIR_NAME)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function isProjectRuntimeActive(cwd) {
|
|
95
|
+
return Boolean(findProjectActivationDir(cwd))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getProjectRoot(cwd) {
|
|
99
|
+
const activeDir = findProjectActivationDir(cwd)
|
|
100
|
+
return activeDir ? dirname(activeDir) : normalizePath(cwd || process.cwd())
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getUserRuntimeRoot(home = getHomeDir()) {
|
|
104
|
+
return join(home, PROJECT_DIR_NAME, USER_RUNTIME_DIR_NAME)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isUserHomeHelloagentsDir(dirPath) {
|
|
108
|
+
const homeCandidates = [
|
|
109
|
+
getHomeDir(),
|
|
110
|
+
process.env.USERPROFILE || '',
|
|
111
|
+
homedir(),
|
|
112
|
+
].filter(Boolean)
|
|
113
|
+
return homeCandidates.some((home) => samePath(dirPath, join(home, PROJECT_DIR_NAME)))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isUserConfigStoreDir(dirPath) {
|
|
117
|
+
return existsSync(join(dirPath, 'helloagents.json'))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isUserHomeDir(dirPath) {
|
|
121
|
+
const homeCandidates = [
|
|
122
|
+
getHomeDir(),
|
|
123
|
+
process.env.USERPROFILE || '',
|
|
124
|
+
homedir(),
|
|
125
|
+
].filter(Boolean)
|
|
126
|
+
return homeCandidates.some((home) => samePath(dirPath, home))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function findProjectActivationDir(cwd) {
|
|
130
|
+
let current = normalizePath(cwd || process.cwd())
|
|
131
|
+
const gitRoot = resolveGitTopLevel(current)
|
|
132
|
+
|
|
133
|
+
while (current) {
|
|
134
|
+
const candidate = join(current, PROJECT_DIR_NAME)
|
|
135
|
+
if (
|
|
136
|
+
existsSync(candidate)
|
|
137
|
+
&& !isUserHomeHelloagentsDir(candidate)
|
|
138
|
+
&& !isUserConfigStoreDir(candidate)
|
|
139
|
+
) {
|
|
140
|
+
return candidate
|
|
141
|
+
}
|
|
142
|
+
if (isUserHomeDir(current)) break
|
|
143
|
+
if (gitRoot && samePath(current, gitRoot)) break
|
|
144
|
+
|
|
145
|
+
const parent = dirname(current)
|
|
146
|
+
if (!parent || parent === current) break
|
|
147
|
+
current = parent
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return ''
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function removePathIfExists(filePath, result, bucket) {
|
|
154
|
+
if (!existsSync(filePath)) return
|
|
155
|
+
try {
|
|
156
|
+
rmSync(filePath, { recursive: true, force: true })
|
|
157
|
+
result[bucket].push(filePath)
|
|
158
|
+
} catch (error) {
|
|
159
|
+
result.errors.push(`${filePath}: ${error.message}`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function cleanupUserRuntimeRoot({
|
|
164
|
+
home = getHomeDir(),
|
|
165
|
+
now = Date.now(),
|
|
166
|
+
maxAgeMs = USER_RUNTIME_MAX_AGE_MS,
|
|
167
|
+
} = {}) {
|
|
168
|
+
const root = getUserRuntimeRoot(home)
|
|
169
|
+
const result = {
|
|
170
|
+
root,
|
|
171
|
+
removedExpiredDirs: [],
|
|
172
|
+
errors: [],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!existsSync(root)) return result
|
|
176
|
+
|
|
177
|
+
let entries = []
|
|
178
|
+
try {
|
|
179
|
+
entries = readdirSync(root, { withFileTypes: true })
|
|
180
|
+
} catch (error) {
|
|
181
|
+
result.errors.push(`${root}: ${error.message}`)
|
|
182
|
+
return result
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
if (!entry.isDirectory()) continue
|
|
187
|
+
const dirPath = join(root, entry.name)
|
|
188
|
+
try {
|
|
189
|
+
if (now - statSync(dirPath).mtimeMs > maxAgeMs) {
|
|
190
|
+
removePathIfExists(dirPath, result, 'removedExpiredDirs')
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
result.errors.push(`${dirPath}: ${error.message}`)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return result
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveStableSessionToken({ payload = {}, env = process.env, ppid = process.ppid } = {}) {
|
|
201
|
+
void ppid
|
|
202
|
+
return resolveSessionToken({
|
|
203
|
+
payload,
|
|
204
|
+
env,
|
|
205
|
+
ppid: 0,
|
|
206
|
+
allowPpidFallback: false,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function resolveTransientSessionToken({ payload = {}, env = process.env, ppid = process.ppid } = {}) {
|
|
211
|
+
return resolveSessionToken({
|
|
212
|
+
payload,
|
|
213
|
+
env,
|
|
214
|
+
ppid,
|
|
215
|
+
allowPpidFallback: true,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function getProjectSessionScope(cwd, options = {}) {
|
|
220
|
+
const normalizedCwd = normalizePath(cwd || process.cwd())
|
|
221
|
+
const projectRoot = getProjectRoot(normalizedCwd)
|
|
222
|
+
const { payload = {}, env = process.env, ppid = process.ppid } = normalizeRuntimeOptions(options)
|
|
223
|
+
const stableToken = resolveStableSessionToken({ payload, env, ppid })
|
|
224
|
+
const branch = sanitizeRuntimeSegment(resolveGitBranchName(projectRoot), 'detached')
|
|
225
|
+
const session = sanitizeRuntimeSegment(stableToken, DEFAULT_STATE_SESSION_TOKEN)
|
|
226
|
+
const activationDir = getProjectActivationDir(projectRoot)
|
|
227
|
+
const sessionDir = join(activationDir, PROJECT_SESSIONS_DIR_NAME, branch, session)
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
cwd: projectRoot,
|
|
231
|
+
active: isProjectRuntimeActive(projectRoot),
|
|
232
|
+
branch,
|
|
233
|
+
session,
|
|
234
|
+
sessionMode: stableToken ? 'host-session' : 'default',
|
|
235
|
+
activationDir,
|
|
236
|
+
sessionDir,
|
|
237
|
+
statePath: join(sessionDir, 'STATE.md'),
|
|
238
|
+
capsulePath: join(sessionDir, CAPSULE_FILE_NAME),
|
|
239
|
+
eventsPath: join(sessionDir, EVENTS_FILE_NAME),
|
|
240
|
+
artifactsDir: join(sessionDir, PROJECT_ARTIFACTS_DIR_NAME),
|
|
241
|
+
key: `${normalizedCwd}::${branch}::${session}`,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function buildTransientRuntimeDir(cwd, options = {}) {
|
|
246
|
+
const normalizedCwd = normalizePath(cwd || process.cwd())
|
|
247
|
+
const { payload = {}, env = process.env, ppid = process.ppid } = normalizeRuntimeOptions(options)
|
|
248
|
+
const token = sanitizeRuntimeSegment(
|
|
249
|
+
resolveTransientSessionToken({ payload, env, ppid }),
|
|
250
|
+
DEFAULT_STATE_SESSION_TOKEN,
|
|
251
|
+
)
|
|
252
|
+
const hash = createHash('sha1')
|
|
253
|
+
.update(`${normalizedCwd.toLowerCase()}::${token}`)
|
|
254
|
+
.digest('hex')
|
|
255
|
+
.slice(0, 16)
|
|
256
|
+
cleanupUserRuntimeRoot()
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
cwd: normalizedCwd,
|
|
260
|
+
branch: 'transient',
|
|
261
|
+
session: token,
|
|
262
|
+
sessionMode: token === DEFAULT_STATE_SESSION_TOKEN ? 'default' : 'transient-session',
|
|
263
|
+
sessionDir: join(getUserRuntimeRoot(), hash),
|
|
264
|
+
capsulePath: join(getUserRuntimeRoot(), hash, CAPSULE_FILE_NAME),
|
|
265
|
+
eventsPath: join(getUserRuntimeRoot(), hash, EVENTS_FILE_NAME),
|
|
266
|
+
artifactsDir: join(getUserRuntimeRoot(), hash, PROJECT_ARTIFACTS_DIR_NAME),
|
|
267
|
+
key: `${normalizedCwd}::transient::${token}`,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function getRuntimeScope(cwd = process.cwd(), options = {}) {
|
|
272
|
+
const projectScope = getProjectSessionScope(cwd, options)
|
|
273
|
+
if (projectScope.active) {
|
|
274
|
+
return {
|
|
275
|
+
...projectScope,
|
|
276
|
+
scope: 'project-session',
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
...buildTransientRuntimeDir(cwd, options),
|
|
282
|
+
active: false,
|
|
283
|
+
scope: 'user-runtime',
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function getRuntimeFilePath(cwd, fileName, options = {}) {
|
|
288
|
+
return join(getRuntimeScope(cwd, options).sessionDir, fileName)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function getProjectEventsPath(cwd, options = {}) {
|
|
292
|
+
const scope = getProjectSessionScope(cwd, options)
|
|
293
|
+
return scope.active ? scope.eventsPath : ''
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function readJsonFile(filePath, fallback = null) {
|
|
297
|
+
try {
|
|
298
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'))
|
|
299
|
+
} catch {
|
|
300
|
+
return fallback
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function writeJsonFileAtomic(filePath, value) {
|
|
305
|
+
mkdirSync(dirname(filePath), { recursive: true })
|
|
306
|
+
const tmpPath = join(dirname(filePath), `.${Date.now()}-${randomUUID()}.tmp`)
|
|
307
|
+
writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}\n`, 'utf-8')
|
|
308
|
+
renameSync(tmpPath, filePath)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function removeRuntimeFile(filePath) {
|
|
312
|
+
rmSync(filePath, { force: true })
|
|
313
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { basename, dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getProjectSessionScope,
|
|
6
|
+
getRuntimeScope,
|
|
7
|
+
readJsonFile,
|
|
8
|
+
removeRuntimeFile,
|
|
9
|
+
writeJsonFileAtomic,
|
|
10
|
+
} from './runtime-scope.mjs'
|
|
11
|
+
|
|
12
|
+
export { getRuntimeScope }
|
|
13
|
+
|
|
14
|
+
function buildEmptyCapsule(scope) {
|
|
15
|
+
return {
|
|
16
|
+
version: 1,
|
|
17
|
+
scope: scope.scope,
|
|
18
|
+
key: scope.key,
|
|
19
|
+
cwd: scope.cwd,
|
|
20
|
+
branch: scope.branch,
|
|
21
|
+
session: scope.session,
|
|
22
|
+
sessionMode: scope.sessionMode,
|
|
23
|
+
updatedAt: new Date().toISOString(),
|
|
24
|
+
turn: null,
|
|
25
|
+
route: null,
|
|
26
|
+
artifacts: {},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeOptions(options = {}) {
|
|
31
|
+
if (!options || typeof options !== 'object') return {}
|
|
32
|
+
if (options.payload && typeof options.payload === 'object') return options
|
|
33
|
+
return {
|
|
34
|
+
...options,
|
|
35
|
+
payload: options,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getScope(cwd, options = {}) {
|
|
40
|
+
const normalizedOptions = normalizeOptions(options)
|
|
41
|
+
if (normalizedOptions.project === true) {
|
|
42
|
+
return {
|
|
43
|
+
...getProjectSessionScope(cwd, normalizedOptions),
|
|
44
|
+
scope: 'project-session',
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return getRuntimeScope(cwd, normalizedOptions)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getSessionCapsulePath(cwd = process.cwd(), options = {}) {
|
|
51
|
+
return getScope(cwd, options).capsulePath
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getSessionEventsPath(cwd = process.cwd(), options = {}) {
|
|
55
|
+
return getScope(cwd, options).eventsPath
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getSessionArtifactsDir(cwd = process.cwd(), options = {}) {
|
|
59
|
+
return getScope(cwd, options).artifactsDir
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getSessionArtifactPath(cwd, fileName, options = {}) {
|
|
63
|
+
return join(getSessionArtifactsDir(cwd, options), fileName)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getSessionArtifactRelativePath(cwd, fileName, options = {}) {
|
|
67
|
+
const scope = getScope(cwd, options)
|
|
68
|
+
if (scope.scope === 'project-session') {
|
|
69
|
+
return `.helloagents/sessions/${scope.branch}/${scope.session}/artifacts/${fileName}`
|
|
70
|
+
}
|
|
71
|
+
return `~/.helloagents/runtime/${basename(scope.sessionDir)}/artifacts/${fileName}`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function readSessionCapsule(cwd = process.cwd(), options = {}) {
|
|
75
|
+
const scope = getScope(cwd, options)
|
|
76
|
+
const capsule = readJsonFile(scope.capsulePath, null)
|
|
77
|
+
if (!capsule || typeof capsule !== 'object') return buildEmptyCapsule(scope)
|
|
78
|
+
return {
|
|
79
|
+
...buildEmptyCapsule(scope),
|
|
80
|
+
...capsule,
|
|
81
|
+
scope: scope.scope,
|
|
82
|
+
key: scope.key,
|
|
83
|
+
cwd: scope.cwd,
|
|
84
|
+
branch: scope.branch,
|
|
85
|
+
session: scope.session,
|
|
86
|
+
sessionMode: scope.sessionMode,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function writeSessionCapsule(cwd, capsule, options = {}) {
|
|
91
|
+
const scope = getScope(cwd, options)
|
|
92
|
+
const nextCapsule = {
|
|
93
|
+
...buildEmptyCapsule(scope),
|
|
94
|
+
...capsule,
|
|
95
|
+
scope: scope.scope,
|
|
96
|
+
key: scope.key,
|
|
97
|
+
cwd: scope.cwd,
|
|
98
|
+
branch: scope.branch,
|
|
99
|
+
session: scope.session,
|
|
100
|
+
sessionMode: scope.sessionMode,
|
|
101
|
+
updatedAt: new Date().toISOString(),
|
|
102
|
+
}
|
|
103
|
+
writeJsonFileAtomic(scope.capsulePath, nextCapsule)
|
|
104
|
+
return nextCapsule
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function updateSessionCapsule(cwd, updater, options = {}) {
|
|
108
|
+
const current = readSessionCapsule(cwd, options)
|
|
109
|
+
const patch = typeof updater === 'function' ? updater(current) : updater
|
|
110
|
+
return writeSessionCapsule(cwd, {
|
|
111
|
+
...current,
|
|
112
|
+
...(patch || {}),
|
|
113
|
+
}, options)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function readCapsuleSection(cwd, section, options = {}) {
|
|
117
|
+
return readSessionCapsule(cwd, options)[section] || null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function writeCapsuleSection(cwd, section, value, options = {}) {
|
|
121
|
+
return updateSessionCapsule(cwd, (capsule) => ({
|
|
122
|
+
[section]: value,
|
|
123
|
+
[`${section}UpdatedAt`]: new Date().toISOString(),
|
|
124
|
+
artifacts: capsule.artifacts || {},
|
|
125
|
+
}), options)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function clearCapsuleSection(cwd, section, options = {}) {
|
|
129
|
+
const capsulePath = getSessionCapsulePath(cwd, options)
|
|
130
|
+
if (!existsSync(capsulePath)) return false
|
|
131
|
+
|
|
132
|
+
const capsule = readSessionCapsule(cwd, options)
|
|
133
|
+
if (!Object.prototype.hasOwnProperty.call(capsule, section)) return false
|
|
134
|
+
capsule[section] = null
|
|
135
|
+
capsule[`${section}UpdatedAt`] = new Date().toISOString()
|
|
136
|
+
writeSessionCapsule(cwd, capsule, options)
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function appendSessionEvent(cwd, eventPayload, options = {}) {
|
|
141
|
+
const scope = getScope(cwd, options)
|
|
142
|
+
if (scope.scope === 'project-session' && !scope.active) return ''
|
|
143
|
+
const eventName = eventPayload?.event || ''
|
|
144
|
+
if (!eventName) return ''
|
|
145
|
+
|
|
146
|
+
mkdirSync(dirname(scope.eventsPath), { recursive: true })
|
|
147
|
+
const payload = {
|
|
148
|
+
ts: new Date().toISOString(),
|
|
149
|
+
scope: scope.scope,
|
|
150
|
+
key: scope.key,
|
|
151
|
+
sessionId: scope.session,
|
|
152
|
+
...eventPayload,
|
|
153
|
+
}
|
|
154
|
+
writeFileSync(scope.eventsPath, `${JSON.stringify(payload)}\n`, {
|
|
155
|
+
encoding: 'utf-8',
|
|
156
|
+
flag: 'a',
|
|
157
|
+
})
|
|
158
|
+
return scope.eventsPath
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function resetSessionEvents(cwd, options = {}) {
|
|
162
|
+
const scope = getScope(cwd, options)
|
|
163
|
+
if (scope.scope === 'project-session' && !scope.active) return ''
|
|
164
|
+
mkdirSync(dirname(scope.eventsPath), { recursive: true })
|
|
165
|
+
writeFileSync(scope.eventsPath, '', 'utf-8')
|
|
166
|
+
return scope.eventsPath
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function readSessionArtifact(cwd, fileName, options = {}) {
|
|
170
|
+
return readJsonFile(getSessionArtifactPath(cwd, fileName, options), null)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function writeSessionArtifact(cwd, fileName, payload, options = {}) {
|
|
174
|
+
const artifactPath = getSessionArtifactPath(cwd, fileName, options)
|
|
175
|
+
writeJsonFileAtomic(artifactPath, payload)
|
|
176
|
+
updateSessionCapsule(cwd, (capsule) => ({
|
|
177
|
+
artifacts: {
|
|
178
|
+
...(capsule.artifacts || {}),
|
|
179
|
+
[fileName]: {
|
|
180
|
+
path: getSessionArtifactRelativePath(cwd, fileName, options),
|
|
181
|
+
updatedAt: new Date().toISOString(),
|
|
182
|
+
type: basename(fileName, '.json'),
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
}), options)
|
|
186
|
+
return artifactPath
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function clearSessionArtifact(cwd, fileName, options = {}) {
|
|
190
|
+
const artifactPath = getSessionArtifactPath(cwd, fileName, options)
|
|
191
|
+
rmSync(artifactPath, { force: true })
|
|
192
|
+
const capsuleOptions = normalizeOptions(options)
|
|
193
|
+
const capsule = readSessionCapsule(cwd, capsuleOptions)
|
|
194
|
+
if (capsule.artifacts && Object.prototype.hasOwnProperty.call(capsule.artifacts, fileName)) {
|
|
195
|
+
delete capsule.artifacts[fileName]
|
|
196
|
+
writeSessionCapsule(cwd, capsule, capsuleOptions)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function removeSessionCapsule(cwd, options = {}) {
|
|
201
|
+
removeRuntimeFile(getSessionCapsulePath(cwd, options))
|
|
202
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const scriptPath = join(dirname(fileURLToPath(import.meta.url)), 'turn-state.mjs')
|
|
7
|
+
const result = spawnSync(process.execPath, [scriptPath, ...process.argv.slice(2)], {
|
|
8
|
+
stdio: 'inherit',
|
|
9
|
+
windowsHide: true,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
if (result.error) {
|
|
13
|
+
console.error(result.error.message)
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
process.exit(typeof result.status === 'number' ? result.status : 1)
|