@automagik/genie 4.260407.5 → 4.260407.7
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/.genie/agents/metrics-updater/tools/parse-metrics.py +121 -34
- package/.genie/agents/metrics-updater/tools/update-readme.py +30 -16
- package/README.md +10 -8
- package/dist/genie.js +5 -5
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/src/term-commands/brain.ts +139 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "genie",
|
|
13
|
-
"version": "4.260407.
|
|
13
|
+
"version": "4.260407.7",
|
|
14
14
|
"source": "./plugins/genie",
|
|
15
15
|
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project."
|
|
16
16
|
}
|
|
@@ -69,12 +69,20 @@ def calc_ship_rate(prs_json: list, days: int = 7) -> float:
|
|
|
69
69
|
return round((shipped_first / total) * 100, 0)
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
def calc_loc_changed(repo_root: str) -> int:
|
|
73
|
-
"""Calculate total lines changed (additions + deletions) in last
|
|
72
|
+
def calc_loc_changed(repo_root: str, branches: list[str] | None = None, hours: int = 24) -> int:
|
|
73
|
+
"""Calculate total lines changed (additions + deletions) in last N hours.
|
|
74
|
+
|
|
75
|
+
When branches is provided, counts changes across all listed branches
|
|
76
|
+
(e.g. ['main', 'dev']) to capture work that hasn't been promoted yet.
|
|
77
|
+
"""
|
|
74
78
|
try:
|
|
79
|
+
cmd = ['git', 'log', f'--since={hours} hours ago', '--stat', '--format=']
|
|
80
|
+
if branches:
|
|
81
|
+
cmd.extend(branches)
|
|
82
|
+
else:
|
|
83
|
+
cmd.append('--all')
|
|
75
84
|
result = subprocess.run(
|
|
76
|
-
|
|
77
|
-
capture_output=True, text=True, cwd=repo_root
|
|
85
|
+
cmd, capture_output=True, text=True, cwd=repo_root
|
|
78
86
|
)
|
|
79
87
|
total = 0
|
|
80
88
|
for line in result.stdout.splitlines():
|
|
@@ -91,12 +99,20 @@ def calc_loc_changed(repo_root: str) -> int:
|
|
|
91
99
|
return 0
|
|
92
100
|
|
|
93
101
|
|
|
94
|
-
def calc_commits_24h(repo_root: str) -> int:
|
|
95
|
-
"""Count commits in the last
|
|
102
|
+
def calc_commits_24h(repo_root: str, branches: list[str] | None = None, hours: int = 24) -> int:
|
|
103
|
+
"""Count commits in the last N hours.
|
|
104
|
+
|
|
105
|
+
When branches is provided, counts across all listed branches
|
|
106
|
+
(e.g. ['main', 'dev']) to capture work not yet promoted to main.
|
|
107
|
+
"""
|
|
96
108
|
try:
|
|
109
|
+
cmd = ['git', 'log', f'--since={hours} hours ago', '--oneline']
|
|
110
|
+
if branches:
|
|
111
|
+
cmd.extend(branches)
|
|
112
|
+
else:
|
|
113
|
+
cmd.append('--all')
|
|
97
114
|
result = subprocess.run(
|
|
98
|
-
|
|
99
|
-
capture_output=True, text=True, cwd=repo_root
|
|
115
|
+
cmd, capture_output=True, text=True, cwd=repo_root
|
|
100
116
|
)
|
|
101
117
|
lines = [l for l in result.stdout.strip().splitlines() if l.strip()]
|
|
102
118
|
return len(lines)
|
|
@@ -104,32 +120,98 @@ def calc_commits_24h(repo_root: str) -> int:
|
|
|
104
120
|
return 0
|
|
105
121
|
|
|
106
122
|
|
|
107
|
-
def
|
|
108
|
-
"""Count
|
|
123
|
+
def calc_files_changed(repo_root: str, hours: int = 24, branches: list[str] | None = None) -> int:
|
|
124
|
+
"""Count files changed in the last N hours."""
|
|
125
|
+
try:
|
|
126
|
+
cmd = ['git', 'log', f'--since={hours} hours ago', '--stat', '--format=']
|
|
127
|
+
if branches:
|
|
128
|
+
cmd.extend(branches)
|
|
129
|
+
else:
|
|
130
|
+
cmd.append('--all')
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
cmd, capture_output=True, text=True, cwd=repo_root
|
|
133
|
+
)
|
|
134
|
+
total = 0
|
|
135
|
+
for line in result.stdout.splitlines():
|
|
136
|
+
if 'file' in line and 'changed' in line:
|
|
137
|
+
total += int(line.strip().split()[0])
|
|
138
|
+
return total
|
|
139
|
+
except Exception:
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def calc_npm_releases(package: str, hours: int = 24) -> int:
|
|
144
|
+
"""Count npm releases in the last N hours."""
|
|
109
145
|
try:
|
|
110
|
-
since = (datetime.now(timezone.utc) - timedelta(hours=24)).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
111
146
|
result = subprocess.run(
|
|
112
|
-
['
|
|
113
|
-
'--jq', '.total_count'],
|
|
147
|
+
['npm', 'view', package, 'time', '--json'],
|
|
114
148
|
capture_output=True, text=True
|
|
115
149
|
)
|
|
116
|
-
|
|
150
|
+
if not result.stdout.strip():
|
|
151
|
+
return 0
|
|
152
|
+
data = json.loads(result.stdout)
|
|
153
|
+
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
|
154
|
+
count = 0
|
|
155
|
+
for ver, ts in data.items():
|
|
156
|
+
if ver in ('created', 'modified'):
|
|
157
|
+
continue
|
|
158
|
+
dt = datetime.fromisoformat(ts.replace('Z', '+00:00'))
|
|
159
|
+
if dt >= cutoff:
|
|
160
|
+
count += 1
|
|
161
|
+
return count
|
|
117
162
|
except Exception:
|
|
118
163
|
return 0
|
|
119
164
|
|
|
120
165
|
|
|
121
|
-
def
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
166
|
+
def calc_merged_prs(prs_json: list, hours: int = 24) -> int:
|
|
167
|
+
"""Count merged PRs within the last N hours."""
|
|
168
|
+
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
|
169
|
+
count = 0
|
|
170
|
+
for pr in prs_json:
|
|
171
|
+
if not pr.get('merged_at'):
|
|
172
|
+
continue
|
|
173
|
+
merged = datetime.fromisoformat(pr['merged_at'].replace('Z', '+00:00'))
|
|
174
|
+
if merged >= cutoff:
|
|
175
|
+
count += 1
|
|
176
|
+
return count
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def calc_avg_merge_time_window(prs_json: list, hours: int = 24) -> float:
|
|
180
|
+
"""Calculate average merge time in hours for PRs merged in the last N hours."""
|
|
181
|
+
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
|
182
|
+
durations = []
|
|
183
|
+
for pr in prs_json:
|
|
184
|
+
if not pr.get('merged_at'):
|
|
185
|
+
continue
|
|
186
|
+
merged = datetime.fromisoformat(pr['merged_at'].replace('Z', '+00:00'))
|
|
187
|
+
if merged < cutoff:
|
|
188
|
+
continue
|
|
189
|
+
created = datetime.fromisoformat(pr['created_at'].replace('Z', '+00:00'))
|
|
190
|
+
durations.append((merged - created).total_seconds() / 3600)
|
|
191
|
+
if not durations:
|
|
192
|
+
return 0.0
|
|
193
|
+
return round(sum(durations) / len(durations), 1)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def format_metrics(*, commits_7d: int = 0, loc_7d: int = 0, files_7d: int = 0,
|
|
197
|
+
prs_7d: int = 0, releases_7d: int = 0, avg_merge_7d: float = 0,
|
|
198
|
+
commits_24h: int = 0, loc_24h: int = 0, files_24h: int = 0,
|
|
199
|
+
prs_24h: int = 0, releases_24h: int = 0, avg_merge_24h: float = 0,
|
|
200
|
+
parallel_agents: int = 0) -> dict:
|
|
125
201
|
"""Format metrics into the standard output structure."""
|
|
126
202
|
return {
|
|
127
|
-
'
|
|
128
|
-
'
|
|
129
|
-
'
|
|
130
|
-
'
|
|
203
|
+
'commits_7d': commits_7d,
|
|
204
|
+
'loc_changed_7d': loc_7d,
|
|
205
|
+
'files_changed_7d': files_7d,
|
|
206
|
+
'prs_7d': prs_7d,
|
|
207
|
+
'releases_7d': releases_7d,
|
|
208
|
+
'avg_merge_time_7d': avg_merge_7d,
|
|
131
209
|
'commits_24h': commits_24h,
|
|
210
|
+
'loc_changed_24h': loc_24h,
|
|
211
|
+
'files_changed_24h': files_24h,
|
|
132
212
|
'prs_24h': prs_24h,
|
|
213
|
+
'releases_24h': releases_24h,
|
|
214
|
+
'avg_merge_time_24h': avg_merge_24h,
|
|
133
215
|
'parallel_agents': parallel_agents,
|
|
134
216
|
'updated': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
135
217
|
}
|
|
@@ -142,6 +224,8 @@ def main():
|
|
|
142
224
|
parser.add_argument('--parallel-agents', type=int, default=0,
|
|
143
225
|
help='Number of parallel agents active')
|
|
144
226
|
parser.add_argument('--repo-root', default='.', help='Path to repo root for git log commands')
|
|
227
|
+
parser.add_argument('--branches', nargs='+', default=['main', 'dev'],
|
|
228
|
+
help='Branches to include in git metrics (default: main dev)')
|
|
145
229
|
parser.add_argument('--owner', default='automagik-dev', help='GitHub owner')
|
|
146
230
|
parser.add_argument('--repo', default='genie', help='GitHub repo name')
|
|
147
231
|
parser.add_argument('--from-state', help='Fallback: read last_metrics from state.json')
|
|
@@ -166,21 +250,24 @@ def main():
|
|
|
166
250
|
with open(args.prs_json) as f:
|
|
167
251
|
prs = json.load(f)
|
|
168
252
|
|
|
169
|
-
|
|
170
|
-
avg_merge = calc_avg_merge_time_hours(prs)
|
|
171
|
-
ship_rate = calc_ship_rate(prs)
|
|
172
|
-
loc_changed = calc_loc_changed(args.repo_root)
|
|
173
|
-
commits = calc_commits_24h(args.repo_root)
|
|
174
|
-
prs_count = calc_prs_24h(args.owner, args.repo)
|
|
253
|
+
pkg = f'@{args.owner}/{args.repo}'
|
|
175
254
|
|
|
176
255
|
metrics = format_metrics(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
256
|
+
# 7-day metrics
|
|
257
|
+
commits_7d=calc_commits_24h(args.repo_root, args.branches, hours=168),
|
|
258
|
+
loc_7d=calc_loc_changed(args.repo_root, args.branches, hours=168),
|
|
259
|
+
files_7d=calc_files_changed(args.repo_root, hours=168, branches=args.branches),
|
|
260
|
+
prs_7d=calc_merged_prs(prs, hours=168),
|
|
261
|
+
releases_7d=calc_npm_releases(pkg, hours=168),
|
|
262
|
+
avg_merge_7d=calc_avg_merge_time_window(prs, hours=168),
|
|
263
|
+
# 24h metrics
|
|
264
|
+
commits_24h=calc_commits_24h(args.repo_root, args.branches),
|
|
265
|
+
loc_24h=calc_loc_changed(args.repo_root, args.branches),
|
|
266
|
+
files_24h=calc_files_changed(args.repo_root, branches=args.branches),
|
|
267
|
+
prs_24h=calc_merged_prs(prs),
|
|
268
|
+
releases_24h=calc_npm_releases(pkg),
|
|
269
|
+
avg_merge_24h=calc_avg_merge_time_window(prs),
|
|
180
270
|
parallel_agents=args.parallel_agents,
|
|
181
|
-
loc_changed_24h=loc_changed,
|
|
182
|
-
commits_24h=commits,
|
|
183
|
-
prs_24h=prs_count,
|
|
184
271
|
)
|
|
185
272
|
|
|
186
273
|
output = json.dumps(metrics, indent=2)
|
|
@@ -23,27 +23,41 @@ END_MARKER = '<!-- METRICS:END \u2014 \U0001f9de automagik/genie -->'
|
|
|
23
23
|
def build_metrics_table(metrics: dict) -> str:
|
|
24
24
|
"""Build the markdown metrics table with signature markers."""
|
|
25
25
|
timestamp = metrics.get('updated', datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'))
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
date_str = timestamp[:10] if 'T' in timestamp else timestamp
|
|
27
|
+
|
|
28
|
+
# 7-day metrics
|
|
29
|
+
commits_7d = metrics.get('commits_7d', 0)
|
|
30
|
+
loc_7d = metrics.get('loc_changed_7d', 0)
|
|
31
|
+
files_7d = metrics.get('files_changed_7d', 0)
|
|
32
|
+
prs_7d = metrics.get('prs_7d', 0)
|
|
33
|
+
releases_7d = metrics.get('releases_7d', 0)
|
|
34
|
+
avg_merge_7d = metrics.get('avg_merge_time_7d', 0)
|
|
35
|
+
|
|
36
|
+
# 24h metrics
|
|
37
|
+
commits_24h = metrics.get('commits_24h', 0)
|
|
38
|
+
loc_24h = metrics.get('loc_changed_24h', 0)
|
|
39
|
+
files_24h = metrics.get('files_changed_24h', 0)
|
|
40
|
+
prs_24h = metrics.get('prs_24h', 0)
|
|
41
|
+
releases_24h = metrics.get('releases_24h', metrics.get('releases_per_day', 0))
|
|
42
|
+
avg_merge_24h = metrics.get('avg_merge_time_24h', metrics.get('avg_bugfix_time_hours', 0))
|
|
33
43
|
|
|
34
44
|
start_marker = f'<!-- METRICS:START \u2014 Updated by Genie Metrics Agent at {timestamp} -->'
|
|
35
45
|
|
|
36
46
|
lines = [
|
|
37
47
|
start_marker,
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
f'|
|
|
43
|
-
f'| Lines changed
|
|
44
|
-
f'|
|
|
45
|
-
f'|
|
|
46
|
-
f'|
|
|
48
|
+
'<p align="center">',
|
|
49
|
+
'',
|
|
50
|
+
'| Metric | 7 days | 24h |',
|
|
51
|
+
'|--------|--------|-----|',
|
|
52
|
+
f'| Commits | {commits_7d:,} | {commits_24h} |',
|
|
53
|
+
f'| Lines changed | {loc_7d:,} | {loc_24h:,} |',
|
|
54
|
+
f'| Files touched | {files_7d:,} | {files_24h} |',
|
|
55
|
+
f'| Merged PRs | {prs_7d} | {prs_24h} |',
|
|
56
|
+
f'| npm releases | {releases_7d} | {releases_24h} |',
|
|
57
|
+
f'| Avg merge time | {avg_merge_7d}h | {avg_merge_24h}h |',
|
|
58
|
+
'',
|
|
59
|
+
f'*Last updated: {date_str}*',
|
|
60
|
+
'</p>',
|
|
47
61
|
END_MARKER,
|
|
48
62
|
]
|
|
49
63
|
return '\n'.join(lines)
|
package/README.md
CHANGED
|
@@ -27,14 +27,16 @@
|
|
|
27
27
|
<!-- METRICS:START -->
|
|
28
28
|
<p align="center">
|
|
29
29
|
|
|
30
|
-
| Metric |
|
|
31
|
-
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
| Metric | 7 days | 24h |
|
|
31
|
+
|--------|--------|-----|
|
|
32
|
+
| Commits | 445 | 38 |
|
|
33
|
+
| Lines changed | 57,462 | 3,917 |
|
|
34
|
+
| Files touched | 1,062 | 80 |
|
|
35
|
+
| Merged PRs | 87 | 10 |
|
|
36
|
+
| npm releases | 68 | 7 |
|
|
37
|
+
| Avg merge time | 3.3h | 5.4h |
|
|
38
|
+
|
|
39
|
+
*Last updated: 2026-04-07*
|
|
38
40
|
</p>
|
|
39
41
|
<!-- METRICS:END -->
|
|
40
42
|
|
package/dist/genie.js
CHANGED
|
@@ -3023,9 +3023,9 @@ Board: ${board.name} (${board.id})`),board.description)console.log(`Description:
|
|
|
3023
3023
|
Columns:`);for(let i2=0;i2<sorted.length;i2++){let c=sorted[i2],count=countByColumn.get(c.name)??countByColumn.get(c.id)??0,gate=` [gate: ${c.gate}]`,action=c.action?` (action: ${c.action})`:"";console.log(` ${i2+1}. ${c.label??c.name}${gate}${action} \u2014 ${count} task${count===1?"":"s"}`)}console.log("")}function buildColumnUpdates(options){let updates={};if(options.gate)updates.gate=options.gate;if(options.action)updates.action=options.action;if(options.color)updates.color=options.color;if(options.rename)updates.name=options.rename,updates.label=options.rename;return updates}async function handleBoardEdit(name,options){let bs=await getBoardService(),board=await resolveBoard(name,options.project);if(options.column){let col=board.columns.find((c)=>c.name===options.column||c.label===options.column);if(!col)throw Error(`Column not found: ${options.column}`);let updates=buildColumnUpdates(options);if(!await bs.updateColumn(board.id,col.id,updates))throw Error(`Failed to update column: ${options.column}`);console.log(`Updated column "${options.column}" on board "${board.name}".`);return}let boardUpdates={};if(options.name)boardUpdates.name=options.name;if(options.description)boardUpdates.description=options.description;if(Object.keys(boardUpdates).length===0)console.error("Error: No updates specified. Use --column, --name, or --description."),process.exit(1);let updated=await bs.updateBoard(board.id,boardUpdates);if(!updated)throw Error(`Failed to update board: ${name}`);console.log(`Updated board "${updated.name}" (${updated.id}).`)}async function handleBoardDelete(name,options){let bs=await getBoardService(),board=await resolveBoard(name,options.project);if(!options.force)console.log(`Deleting board "${board.name}" (${board.id})...`);if(!await bs.deleteBoard(board.id))throw Error(`Failed to delete board: ${name}`);console.log(`Deleted board "${board.name}" (${board.id}).`)}async function handleBoardColumns(name,options){let board=await resolveBoard(name,options.project);if(options.json){console.log(JSON.stringify(board.columns,null,2));return}printColumnPipeline(board.columns,`Board: ${board.name} (${board.columns.length} columns)`)}async function handleBoardUse(name,options){let board=await resolveBoard(name,options.project),repoRoot=execSync10("git rev-parse --show-toplevel",{encoding:"utf-8"}).trim(),genieDir=join37(repoRoot,".genie"),configPath2=join37(genieDir,"config.json");if(!existsSync28(genieDir))mkdirSync12(genieDir,{recursive:!0});let config={};if(existsSync28(configPath2))try{config=JSON.parse(readFileSync17(configPath2,"utf-8"))}catch{}config.activeBoard=board.id,writeFileSync15(configPath2,`${JSON.stringify(config,null,2)}
|
|
3024
3024
|
`),console.log(`Active board set to "${board.name}" (${board.id})`)}async function handleBoardExport(name,options){let bs=await getBoardService(),board=await resolveBoard(name,options.project),exported=await bs.exportBoard(board.id),json2=JSON.stringify(exported,null,2);if(options.output){let dir=dirname6(options.output);if(!existsSync28(dir))mkdirSync12(dir,{recursive:!0});writeFileSync15(options.output,`${json2}
|
|
3025
3025
|
`),console.log(`Exported board "${board.name}" to ${options.output}`)}else console.log(json2)}async function handleBoardReconcile(name,options){let{reconcileBoard:reconcileBoard2}=await Promise.resolve().then(() => (init_board_service(),exports_board_service)),board=await resolveBoard(name,options.project),result2=await reconcileBoard2(board.id);if(options.json){console.log(JSON.stringify(result2,null,2));return}if(result2.fixed===0&&result2.orphaned===0){console.log(`Board "${board.name}": all tasks have valid column_ids.`);return}if(console.log(`Board "${board.name}" reconciliation:`),console.log(` Fixed: ${result2.fixed} task${result2.fixed===1?"":"s"}`),result2.orphaned>0){let count=result2.orphaned;console.log(` Still orphaned: ${count} task${count===1?"":"s"} (stage doesn't match any column)`)}}async function handleBoardImport(options){let bs=await getBoardService(),projectId=await resolveProjectId(options.project),raw=readFileSync17(options.json,"utf-8"),data=JSON.parse(raw),board=await bs.importBoard(data,projectId);console.log(`Imported board "${board.name}" (${board.id}) with ${board.columns.length} columns`)}async function handleTemplateList(options){let templates=await(await getTemplateService()).listTemplates();if(options.json){console.log(JSON.stringify(templates,null,2));return}printTemplateTable(templates)}async function handleTemplateShow(name,options){let template=await(await getTemplateService()).getTemplate(name);if(!template)throw Error(`Template not found: ${name}`);if(options.json){console.log(JSON.stringify(template,null,2));return}if(console.log(`
|
|
3026
|
-
Template: ${template.name} (${template.id})`),template.description)console.log(`Description: ${template.description}`);if(template.icon)console.log(`Icon: ${template.icon}`);console.log(`Built-in: ${template.isBuiltin?"yes":"no"}`),printColumnPipeline(template.columns,`Pipeline (${template.columns.length} columns)`)}async function handleTemplateCreate(name,options){let tmpl=await getTemplateService();if(options.fromBoard){let board=await(await getBoardService()).getBoard(options.fromBoard);if(!board)throw Error(`Board not found: ${options.fromBoard}`);let template2=await tmpl.snapshotFromBoard(board.id,name);console.log(`Created template "${template2.name}" (${template2.id}) from board "${board.name}" with ${template2.columns.length} columns`);return}let columns;if(options.columns)columns=options.columns.split(",").map((colName,i2)=>({id:crypto.randomUUID(),name:colName.trim(),label:colName.trim(),gate:"human",action:null,auto_advance:!1,transitions:[],roles:["*"],color:"#94a3b8",parallel:!1,on_fail:null,position:i2}));let template=await tmpl.createTemplate({name,description:options.description,columns});console.log(`Created template "${template.name}" (${template.id}) with ${template.columns.length} columns`)}async function handleTemplateEdit(name,options){let tmpl=await getTemplateService(),template=await tmpl.getTemplate(name);if(!template)throw Error(`Template not found: ${name}`);if(!options.column)console.error("Error: --column is required for template edit."),process.exit(1);let updates={};if(options.gate)updates.gate=options.gate;if(options.action)updates.action=options.action;if(options.color)updates.color=options.color;if(options.rename)updates.name=options.rename,updates.label=options.rename;if(!await tmpl.updateTemplateColumn(template.id,options.column,updates))throw Error(`Failed to update template: ${name}`);console.log(`Updated column "${options.column}" on template "${template.name}".`)}async function handleTemplateRename(oldName,newName){let tmpl=await getTemplateService(),template=await tmpl.getTemplate(oldName);if(!template)throw Error(`Template not found: ${oldName}`);let updated=await tmpl.renameTemplate(template.id,newName);if(!updated)throw Error(`Failed to rename template: ${oldName}`);console.log(`Renamed template "${oldName}" to "${updated.name}".`)}async function handleTemplateDelete(name){let tmpl=await getTemplateService(),template=await tmpl.getTemplate(name);if(!template)throw Error(`Template not found: ${name}`);if(!await tmpl.deleteTemplate(template.id))throw Error(`Failed to delete template: ${name}`);console.log(`Deleted template "${template.name}" (${template.id}).`)}function registerBoardCommands(program2){let board=program2.command("board").description("Board and pipeline management");board.command("create <name>").description("Create a new board").option("--project <project>","Project name").option("--from <template>","Create from template name").option("--columns <columns>","Comma-separated column names").option("--description <text>","Board description").action(async(name,options)=>{try{await handleBoardCreate(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("list").description("List all boards").option("--project <project>","Filter by project").option("--all","Include archived boards").option("--json","Output as JSON").action(async(options)=>{try{await handleBoardList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("show <name...>").description("Show board detail").option("--project <project>","Disambiguate by project").option("--json","Output as JSON").action(async(nameParts,options)=>{try{await handleBoardShow(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("edit <name...>").description("Edit board or column properties").option("--project <project>","Disambiguate by project").option("--column <col>","Column name to edit").option("--gate <gate>","New gate value (human|agent|human+agent)").option("--action <action>","New action skill").option("--color <color>","New color hex").option("--rename <new>","Rename the column").option("--name <new>","Rename the board itself").option("--description <text>","Update description").action(async(nameParts,options)=>{try{await handleBoardEdit(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("delete <name...>").description("Delete a board").option("--project <project>","Disambiguate by project").option("--force","Skip confirmation").action(async(nameParts,options)=>{try{await handleBoardDelete(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("columns <name...>").description("Show board column pipeline").option("--project <project>","Disambiguate by project").option("--json","Output as JSON").action(async(nameParts,options)=>{try{await handleBoardColumns(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("use <name...>").description("Set active board for current repo").option("--project <project>","Disambiguate by project").action(async(nameParts,options)=>{try{await handleBoardUse(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("export <name...>").description("Export board as JSON").option("--project <project>","Disambiguate by project").option("--output <file>","Write to file instead of stdout").option("--json","Output as JSON (default, accepted for consistency)").action(async(nameParts,options)=>{try{await handleBoardExport(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("reconcile <name...>").description("Fix orphaned column_ids by matching task stage to board columns").option("--project <project>","Disambiguate by project").option("--json","Output as JSON").action(async(nameParts,options)=>{try{await handleBoardReconcile(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("archive <name...>").description("Archive a board and its unfinished tasks").option("--project <project>","Disambiguate by project").action(async(nameParts,options)=>{try{let ts3=await getTaskService3(),board2=await resolveBoard(nameParts.join(" "),options.project);await ts3.archiveBoard(board2.id),console.log(`Archived board "${board2.name}" and its unfinished tasks.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("import").description("Import board from JSON file").requiredOption("--json <file>","JSON file to import").requiredOption("--project <project>","Target project").action(async(options)=>{try{await handleBoardImport(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}});let template=board.command("template").description("Board template management");template.command("list").description("List all board templates").option("--json","Output as JSON").action(async(options)=>{try{await handleTemplateList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("show <name>").description("Show template detail with pipeline view").option("--json","Output as JSON").action(async(name,options)=>{try{await handleTemplateShow(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("create <name>").description("Create a board template").option("--from-board <board>","Create from existing board").option("--columns <columns>","Comma-separated column names").option("--description <text>","Template description").action(async(name,options)=>{try{await handleTemplateCreate(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("edit <name>").description("Edit a template column").option("--column <col>","Column name to edit").option("--gate <gate>","New gate value (human|agent|human+agent)").option("--action <action>","New action skill").option("--rename <new>","Rename the column").option("--color <color>","New color hex").action(async(name,options)=>{try{await handleTemplateEdit(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("rename <old> <new>").description("Rename a template").action(async(oldName,newName)=>{try{await handleTemplateRename(oldName,newName)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("delete <name>").description("Delete a template").action(async(name)=>{try{await handleTemplateDelete(name)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}import{execSync as execSync11}from"child_process";import{existsSync as existsSync29,mkdirSync as mkdirSync13,readFileSync as readFileSync18,realpathSync as realpathSync4,writeFileSync as writeFileSync16}from"fs";import{homedir as homedir25}from"os";import{dirname as dirname7,join as join38,resolve as resolve6}from"path";var BRAIN_PKG2="@khal-os/brain",BRAIN_REPO="khal-os/brain";function resolveGenieRoot(){try{let scriptDir=dirname7(realpathSync4(process.argv[1])),candidates=[resolve6(scriptDir,".."),resolve6(scriptDir,"..","..")];for(let c of candidates)if(existsSync29(join38(c,"package.json")))return c}catch{}return resolve6(import.meta.dir,"..","..")}var BRAIN_DIR2=join38(resolveGenieRoot(),"node_modules","@khal-os","brain"),CACHE_PATH=join38(homedir25(),".genie","brain-version-check.json");function compareVersions(a,b2){let partsA=a.split(".").map(Number),partsB=b2.split(".").map(Number);for(let i2=0;i2<Math.max(partsA.length,partsB.length);i2++){let diff=(partsA[i2]??0)-(partsB[i2]??0);if(diff!==0)return diff}return 0}function readLocalBrainVersion(){try{return JSON.parse(readFileSync18(join38(BRAIN_DIR2,"package.json"),"utf-8")).version??"unknown"}catch{return}}function checkForUpdates(cachePath){try{let p=cachePath??CACHE_PATH;if(!existsSync29(p))return{updateAvailable:!1};let cache=JSON.parse(readFileSync18(p,"utf-8"));if(cache.updateAvailable&&cache.latestVersion)return{updateAvailable:!0,latestVersion:cache.latestVersion};return{updateAvailable:!1}}catch{return{updateAvailable:!1}}}function refreshVersionCache(localVersion){try{let version=localVersion??readLocalBrainVersion();if(!version)return;let latestTag=execSync11(`gh release view --repo ${BRAIN_REPO} --json tagName -q .tagName`,{stdio:"pipe",encoding:"utf-8"}).trim(),latestVersion=latestTag.replace(/^v/,""),localCore=version.replace(/^\d+\./,""),latestCore=latestVersion.replace(/^\d+\./,""),updateAvailable=compareVersions(latestCore,localCore)>0,cacheDir=join38(homedir25(),".genie");mkdirSync13(cacheDir,{recursive:!0}),writeFileSync16(CACHE_PATH,JSON.stringify({checkedAt:new Date().toISOString(),localVersion:version,latestTag,latestVersion,updateAvailable},null,2))}catch{}}async function updateBrain(){if(!existsSync29(join38(BRAIN_DIR2,"package.json")))return console.log(" Brain is not installed. Run: genie brain install"),!1;let oldVersion=readLocalBrainVersion()??"unknown";console.log(" Checking for updates...");let tag;try{tag=execSync11(`gh release view --repo ${BRAIN_REPO} --json tagName -q .tagName`,{stdio:"pipe",encoding:"utf-8"}).trim()}catch{return console.error(" Failed to check latest release. Ensure: gh auth login"),!1}let newVersion=tag.replace(/^v/,"");if(compareVersions(newVersion,oldVersion)<=0)return console.log(` Already at latest version (${oldVersion}).`),!0;console.log(` Upgrading: ${oldVersion} \u2192 ${newVersion}`),console.log("");let tmpDir=join38(homedir25(),".cache","genie-brain");mkdirSync13(tmpDir,{recursive:!0}),execSync11(`gh release download ${tag} --repo ${BRAIN_REPO} --pattern '*.tgz' --dir "${tmpDir}" --clobber`,{stdio:"inherit"}),execSync11(`rm -rf "${BRAIN_DIR2}"`,{stdio:"pipe"}),mkdirSync13(BRAIN_DIR2,{recursive:!0}),execSync11(`tar xzf "${tmpDir}/khal-os-brain-${newVersion}.tgz" -C "${BRAIN_DIR2}" --strip-components=1`,{stdio:"inherit"}),execSync11("bun install",{cwd:BRAIN_DIR2,stdio:"inherit"}),console.log(`
|
|
3027
|
-
Updated: ${oldVersion} \u2192 ${newVersion}`);try{let migrateScript=`const b = require('${BRAIN_PKG2}'); if (b.runAllMigrations) b.runAllMigrations().then(() => process.exit(0)).catch((e) => { console.error(e); process.exit(1); }); else process.exit(0);`;execSync11(`bun -e "${migrateScript}"`,{cwd:BRAIN_DIR2,stdio:"inherit"}),console.log(" Migrations applied.")}catch{console.log(" Migration skipped. Run: genie brain migrate")}
|
|
3028
|
-
Update available (${check2.latestVersion}). Run: genie brain upgrade`)}else console.error("Brain module loaded but execute() not found."),console.error("Update: genie brain install")}catch(err){let msg=err instanceof Error?err.message:String(err);if(isModuleNotFound(msg))printNotInstalledMessage();else console.error(`Brain error: ${msg}`)}}function registerBrainCommands(program2){let brain=program2.command("brain").description("Knowledge graph engine (enterprise)").allowUnknownOption().allowExcessArguments().action(async(_options,cmd)=>{let args=cmd.args;if(args.length===0){brain.help();return}await executeBrainCommand(args)});brain.command("install").description("Install genie-brain from GitHub").action(async()=>{await installBrain()}),brain.command("uninstall").description("Remove genie-brain installation").action(()=>{uninstallBrain()}),brain.command("upgrade").description("Upgrade genie-brain to latest version").action(async()=>{await updateBrain()}),brain.command("version").description("Show installed brain version").action(async()=>{await showVersion()})}var _brief2;async function getBrief2(){if(!_brief2)_brief2=await Promise.resolve().then(() => (init_brief(),exports_brief));return _brief2}async function handleBrief2(options){let team=options.team??process.env.GENIE_TEAM;if(!team)console.error("Error: --team is required (or set GENIE_TEAM)"),process.exit(1);let agent=options.agent??process.env.GENIE_AGENT_NAME,briefService=await getBrief2(),brief=await briefService.generateBrief({team,agent,since:options.since,repoPath:process.cwd()});console.log(briefService.formatBrief(brief))}function registerBriefCommands(program2){program2.command("brief").description("Show startup brief \u2014 aggregated context since last session").option("--team <name>","Team name (default: GENIE_TEAM)").option("--agent <name>","Agent name (default: GENIE_AGENT_NAME)").option("--since <iso>","Start timestamp (default: last executor end)").action(async(options)=>{try{await handleBrief2(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}import{existsSync as existsSync30,mkdirSync as mkdirSync14,readFileSync as readFileSync19,unlinkSync as unlinkSync8,writeFileSync as writeFileSync17}from"fs";import{homedir as homedir26}from"os";import{join as join39}from"path";function genieHome4(){return process.env.GENIE_HOME??join39(homedir26(),".genie")}function pidFilePath(){return join39(genieHome4(),"scheduler.pid")}function logFilePath(){return join39(genieHome4(),"logs","scheduler.log")}function systemdDir(){return join39(homedir26(),".config","systemd","user")}function systemdUnitPath(){return join39(systemdDir(),"genie-scheduler.service")}function readPid(){let path3=pidFilePath();if(!existsSync30(path3))return null;let raw=readFileSync19(path3,"utf-8").trim(),pid=Number.parseInt(raw,10);if(Number.isNaN(pid)||pid<=0)return null;return pid}function removePid(){let path3=pidFilePath();if(existsSync30(path3))try{unlinkSync8(path3)}catch{}}function
|
|
3026
|
+
Template: ${template.name} (${template.id})`),template.description)console.log(`Description: ${template.description}`);if(template.icon)console.log(`Icon: ${template.icon}`);console.log(`Built-in: ${template.isBuiltin?"yes":"no"}`),printColumnPipeline(template.columns,`Pipeline (${template.columns.length} columns)`)}async function handleTemplateCreate(name,options){let tmpl=await getTemplateService();if(options.fromBoard){let board=await(await getBoardService()).getBoard(options.fromBoard);if(!board)throw Error(`Board not found: ${options.fromBoard}`);let template2=await tmpl.snapshotFromBoard(board.id,name);console.log(`Created template "${template2.name}" (${template2.id}) from board "${board.name}" with ${template2.columns.length} columns`);return}let columns;if(options.columns)columns=options.columns.split(",").map((colName,i2)=>({id:crypto.randomUUID(),name:colName.trim(),label:colName.trim(),gate:"human",action:null,auto_advance:!1,transitions:[],roles:["*"],color:"#94a3b8",parallel:!1,on_fail:null,position:i2}));let template=await tmpl.createTemplate({name,description:options.description,columns});console.log(`Created template "${template.name}" (${template.id}) with ${template.columns.length} columns`)}async function handleTemplateEdit(name,options){let tmpl=await getTemplateService(),template=await tmpl.getTemplate(name);if(!template)throw Error(`Template not found: ${name}`);if(!options.column)console.error("Error: --column is required for template edit."),process.exit(1);let updates={};if(options.gate)updates.gate=options.gate;if(options.action)updates.action=options.action;if(options.color)updates.color=options.color;if(options.rename)updates.name=options.rename,updates.label=options.rename;if(!await tmpl.updateTemplateColumn(template.id,options.column,updates))throw Error(`Failed to update template: ${name}`);console.log(`Updated column "${options.column}" on template "${template.name}".`)}async function handleTemplateRename(oldName,newName){let tmpl=await getTemplateService(),template=await tmpl.getTemplate(oldName);if(!template)throw Error(`Template not found: ${oldName}`);let updated=await tmpl.renameTemplate(template.id,newName);if(!updated)throw Error(`Failed to rename template: ${oldName}`);console.log(`Renamed template "${oldName}" to "${updated.name}".`)}async function handleTemplateDelete(name){let tmpl=await getTemplateService(),template=await tmpl.getTemplate(name);if(!template)throw Error(`Template not found: ${name}`);if(!await tmpl.deleteTemplate(template.id))throw Error(`Failed to delete template: ${name}`);console.log(`Deleted template "${template.name}" (${template.id}).`)}function registerBoardCommands(program2){let board=program2.command("board").description("Board and pipeline management");board.command("create <name>").description("Create a new board").option("--project <project>","Project name").option("--from <template>","Create from template name").option("--columns <columns>","Comma-separated column names").option("--description <text>","Board description").action(async(name,options)=>{try{await handleBoardCreate(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("list").description("List all boards").option("--project <project>","Filter by project").option("--all","Include archived boards").option("--json","Output as JSON").action(async(options)=>{try{await handleBoardList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("show <name...>").description("Show board detail").option("--project <project>","Disambiguate by project").option("--json","Output as JSON").action(async(nameParts,options)=>{try{await handleBoardShow(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("edit <name...>").description("Edit board or column properties").option("--project <project>","Disambiguate by project").option("--column <col>","Column name to edit").option("--gate <gate>","New gate value (human|agent|human+agent)").option("--action <action>","New action skill").option("--color <color>","New color hex").option("--rename <new>","Rename the column").option("--name <new>","Rename the board itself").option("--description <text>","Update description").action(async(nameParts,options)=>{try{await handleBoardEdit(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("delete <name...>").description("Delete a board").option("--project <project>","Disambiguate by project").option("--force","Skip confirmation").action(async(nameParts,options)=>{try{await handleBoardDelete(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("columns <name...>").description("Show board column pipeline").option("--project <project>","Disambiguate by project").option("--json","Output as JSON").action(async(nameParts,options)=>{try{await handleBoardColumns(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("use <name...>").description("Set active board for current repo").option("--project <project>","Disambiguate by project").action(async(nameParts,options)=>{try{await handleBoardUse(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("export <name...>").description("Export board as JSON").option("--project <project>","Disambiguate by project").option("--output <file>","Write to file instead of stdout").option("--json","Output as JSON (default, accepted for consistency)").action(async(nameParts,options)=>{try{await handleBoardExport(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("reconcile <name...>").description("Fix orphaned column_ids by matching task stage to board columns").option("--project <project>","Disambiguate by project").option("--json","Output as JSON").action(async(nameParts,options)=>{try{await handleBoardReconcile(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("archive <name...>").description("Archive a board and its unfinished tasks").option("--project <project>","Disambiguate by project").action(async(nameParts,options)=>{try{let ts3=await getTaskService3(),board2=await resolveBoard(nameParts.join(" "),options.project);await ts3.archiveBoard(board2.id),console.log(`Archived board "${board2.name}" and its unfinished tasks.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("import").description("Import board from JSON file").requiredOption("--json <file>","JSON file to import").requiredOption("--project <project>","Target project").action(async(options)=>{try{await handleBoardImport(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}});let template=board.command("template").description("Board template management");template.command("list").description("List all board templates").option("--json","Output as JSON").action(async(options)=>{try{await handleTemplateList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("show <name>").description("Show template detail with pipeline view").option("--json","Output as JSON").action(async(name,options)=>{try{await handleTemplateShow(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("create <name>").description("Create a board template").option("--from-board <board>","Create from existing board").option("--columns <columns>","Comma-separated column names").option("--description <text>","Template description").action(async(name,options)=>{try{await handleTemplateCreate(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("edit <name>").description("Edit a template column").option("--column <col>","Column name to edit").option("--gate <gate>","New gate value (human|agent|human+agent)").option("--action <action>","New action skill").option("--rename <new>","Rename the column").option("--color <color>","New color hex").action(async(name,options)=>{try{await handleTemplateEdit(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("rename <old> <new>").description("Rename a template").action(async(oldName,newName)=>{try{await handleTemplateRename(oldName,newName)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("delete <name>").description("Delete a template").action(async(name)=>{try{await handleTemplateDelete(name)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}import{execSync as execSync11}from"child_process";import{existsSync as existsSync29,mkdirSync as mkdirSync13,readFileSync as readFileSync18,realpathSync as realpathSync4,writeFileSync as writeFileSync16}from"fs";import{homedir as homedir25}from"os";import{dirname as dirname7,join as join38,resolve as resolve6}from"path";var BRAIN_PKG2="@khal-os/brain",BRAIN_REPO="khal-os/brain";function resolveGenieRoot(){try{let scriptDir=dirname7(realpathSync4(process.argv[1])),candidates=[resolve6(scriptDir,".."),resolve6(scriptDir,"..","..")];for(let c of candidates)if(existsSync29(join38(c,"package.json")))return c}catch{}return resolve6(import.meta.dir,"..","..")}var BRAIN_DIR2=join38(resolveGenieRoot(),"node_modules","@khal-os","brain"),CACHE_PATH=join38(homedir25(),".genie","brain-version-check.json");function compareVersions(a,b2){let partsA=a.split(".").map(Number),partsB=b2.split(".").map(Number);for(let i2=0;i2<Math.max(partsA.length,partsB.length);i2++){let diff=(partsA[i2]??0)-(partsB[i2]??0);if(diff!==0)return diff}return 0}function readLocalBrainVersion(){try{return JSON.parse(readFileSync18(join38(BRAIN_DIR2,"package.json"),"utf-8")).version??"unknown"}catch{return}}function resolveBrainBin(){let candidates=[join38(resolveGenieRoot(),"node_modules",".bin","brain"),join38(BRAIN_DIR2,"dist","cli.js")];for(let c of candidates)if(existsSync29(c))return c;return}function findBrainVault(){let candidates=[process.cwd(),join38(process.cwd(),"brain"),join38(homedir25(),"brain")];for(let dir of candidates)if(existsSync29(join38(dir,"brain.json")))return dir;return null}function readActiveBrainConfig(){try{let configPath2=join38(homedir25(),".brain","config.json");if(!existsSync29(configPath2))return null;let data=JSON.parse(readFileSync18(configPath2,"utf-8"));if(!data.pid)return null;return{pid:data.pid,pgPort:data.pgPort,brainPath:data.brainPath}}catch{return null}}function isProcessAlive2(pid){try{return process.kill(pid,0),!0}catch{return!1}}async function stopBrainDaemon(){let config=readActiveBrainConfig();if(!config?.pid||!isProcessAlive2(config.pid))return!1;let brainBin=resolveBrainBin(),brainPath=config.brainPath;if(brainBin)try{let pathArg=brainPath?` --brain-path "${brainPath}"`:"";execSync11(`"${brainBin}" serve stop${pathArg}`,{stdio:"pipe",timeout:1e4})}catch{}for(let i2=0;i2<25;i2++){if(!isProcessAlive2(config.pid))return!0;await new Promise((r)=>setTimeout(r,200))}try{process.kill(config.pid,"SIGKILL")}catch{}return!0}function startBrainDaemon(vaultPath,extraArgs){let bin=resolveBrainBin();if(!bin)return;try{let argsStr=extraArgs?.length?` ${extraArgs.join(" ")}`:"";execSync11(`"${bin}" serve --daemon --brain-path "${vaultPath}"${argsStr}`,{stdio:"inherit",timeout:15000}),console.log(" Brain daemon started.")}catch{console.log(" Daemon failed to start. Run: brain serve --daemon")}}function readSavedDaemonArgs(brainPath){try{let serverJsonPath=join38(brainPath,".brain-server.json");return JSON.parse(readFileSync18(serverJsonPath,"utf-8")).args}catch{return}}function checkForUpdates(cachePath){try{let p=cachePath??CACHE_PATH;if(!existsSync29(p))return{updateAvailable:!1};let cache=JSON.parse(readFileSync18(p,"utf-8"));if(cache.updateAvailable&&cache.latestVersion)return{updateAvailable:!0,latestVersion:cache.latestVersion};return{updateAvailable:!1}}catch{return{updateAvailable:!1}}}function refreshVersionCache(localVersion){try{let version=localVersion??readLocalBrainVersion();if(!version)return;let latestTag=execSync11(`gh release view --repo ${BRAIN_REPO} --json tagName -q .tagName`,{stdio:"pipe",encoding:"utf-8"}).trim(),latestVersion=latestTag.replace(/^v/,""),localCore=version.replace(/^\d+\./,""),latestCore=latestVersion.replace(/^\d+\./,""),updateAvailable=compareVersions(latestCore,localCore)>0,cacheDir=join38(homedir25(),".genie");mkdirSync13(cacheDir,{recursive:!0}),writeFileSync16(CACHE_PATH,JSON.stringify({checkedAt:new Date().toISOString(),localVersion:version,latestTag,latestVersion,updateAvailable},null,2))}catch{}}async function updateBrain(){if(!existsSync29(join38(BRAIN_DIR2,"package.json")))return console.log(" Brain is not installed. Run: genie brain install"),!1;let oldVersion=readLocalBrainVersion()??"unknown";console.log(" Checking for updates...");let tag;try{tag=execSync11(`gh release view --repo ${BRAIN_REPO} --json tagName -q .tagName`,{stdio:"pipe",encoding:"utf-8"}).trim()}catch{return console.error(" Failed to check latest release. Ensure: gh auth login"),!1}let newVersion=tag.replace(/^v/,"");if(compareVersions(newVersion,oldVersion)<=0)return console.log(` Already at latest version (${oldVersion}).`),!0;console.log(` Upgrading: ${oldVersion} \u2192 ${newVersion}`),console.log("");let activeConfig=readActiveBrainConfig(),savedArgs=activeConfig?.brainPath?readSavedDaemonArgs(activeConfig.brainPath):void 0,wasRunning=activeConfig?await stopBrainDaemon():!1,tmpDir=join38(homedir25(),".cache","genie-brain");mkdirSync13(tmpDir,{recursive:!0}),execSync11(`gh release download ${tag} --repo ${BRAIN_REPO} --pattern '*.tgz' --dir "${tmpDir}" --clobber`,{stdio:"inherit"}),execSync11(`rm -rf "${BRAIN_DIR2}"`,{stdio:"pipe"}),mkdirSync13(BRAIN_DIR2,{recursive:!0}),execSync11(`tar xzf "${tmpDir}/khal-os-brain-${newVersion}.tgz" -C "${BRAIN_DIR2}" --strip-components=1`,{stdio:"inherit"}),execSync11("bun install",{cwd:BRAIN_DIR2,stdio:"inherit"}),console.log(`
|
|
3027
|
+
Updated: ${oldVersion} \u2192 ${newVersion}`);try{let migrateScript=`const b = require('${BRAIN_PKG2}'); if (b.runAllMigrations) b.runAllMigrations().then(() => process.exit(0)).catch((e) => { console.error(e); process.exit(1); }); else process.exit(0);`;execSync11(`bun -e "${migrateScript}"`,{cwd:BRAIN_DIR2,stdio:"inherit"}),console.log(" Migrations applied.")}catch{console.log(" Migration skipped. Run: genie brain migrate")}if(refreshVersionCache(newVersion),wasRunning&&activeConfig?.brainPath)startBrainDaemon(activeConfig.brainPath,savedArgs);return!0}async function showVersion(){let localVersion="not installed";try{let brain=await import(BRAIN_PKG2);localVersion=brain.getVersion?.()??brain.VERSION??"unknown"}catch(err){let msg=err instanceof Error?err.message:String(err);if(isModuleNotFound(msg)){console.log(" Brain is not installed. Run: genie brain install");return}}console.log(` Local: ${localVersion}`),refreshVersionCache(localVersion);let check2=checkForUpdates();if(check2.updateAvailable&&check2.latestVersion)console.log(` Latest: ${check2.latestVersion}`),console.log(""),console.log(" Update available. Run: genie brain upgrade");else console.log(" Status: up to date")}async function installBrain(){console.log(""),console.log(" Installing brain from GitHub release (enterprise)..."),console.log(""),console.log(" Source: https://github.com/khal-os/brain"),console.log(" Requires: GitHub org membership (khal-os)"),console.log("");try{try{execSync11("gh auth token",{stdio:"pipe"})}catch{return console.error(" GitHub CLI not authenticated. Run: gh auth login"),!1}let tag=execSync11(`gh release view --repo ${BRAIN_REPO} --json tagName -q .tagName`,{stdio:"pipe",encoding:"utf-8"}).trim(),version=tag.replace(/^v/,"");console.log(` Latest release: ${tag}`),console.log("");let root=resolveGenieRoot(),brainDir=join38(root,"node_modules","@khal-os","brain"),tmpDir=join38(homedir25(),".cache","genie-brain");mkdirSync13(tmpDir,{recursive:!0}),execSync11(`gh release download ${tag} --repo ${BRAIN_REPO} --pattern '*.tgz' --dir "${tmpDir}" --clobber`,{stdio:"inherit"}),execSync11(`rm -rf "${brainDir}"`,{stdio:"pipe"}),mkdirSync13(brainDir,{recursive:!0}),execSync11(`tar xzf "${tmpDir}/khal-os-brain-${version}.tgz" -C "${brainDir}" --strip-components=1`,{stdio:"inherit"}),execSync11("bun install",{cwd:brainDir,stdio:"inherit"}),console.log(""),console.log(` Brain ${version} installed from GitHub release.`),console.log("");try{let brain=await import(BRAIN_PKG2);if(brain.runAllMigrations)console.log(" Running brain migrations..."),await brain.runAllMigrations(),console.log(" Brain tables created in Postgres.")}catch{console.log(" Auto-migration skipped. Run: genie brain migrate")}let vaultPath=findBrainVault();if(vaultPath)startBrainDaemon(vaultPath);else console.log(" No brain vault found. Create one with: brain init --name <name> --path <path>");return console.log(""),console.log(" Get started:"),console.log(" genie brain init --name my-brain --path ./brain"),console.log(""),!0}catch(err){let msg=err instanceof Error?err.message:String(err);if(msg.includes("Authentication")||msg.includes("permission")||msg.includes("404"))console.error(" Access denied. Brain is enterprise-only."),console.log(""),console.log(" You need:"),console.log(" 1. Membership in the khal-os GitHub org"),console.log(" 2. GitHub CLI authenticated: gh auth login"),console.log(""),console.log(" Manual install:"),console.log(` gh release download --repo ${BRAIN_REPO} --pattern '*.tgz'`),console.log(" tar xzf khal-os-brain-*.tgz -C node_modules/@khal-os/brain --strip-components=1"),console.log("");else console.error(` Install failed: ${msg}`),console.log(""),console.log(" Manual install:"),console.log(` gh release download --repo ${BRAIN_REPO} --pattern '*.tgz'`),console.log(" tar xzf khal-os-brain-*.tgz -C node_modules/@khal-os/brain --strip-components=1"),console.log("");return!1}}async function uninstallBrain(){try{if(!existsSync29(BRAIN_DIR2)){console.log(" Brain is not installed.");return}await stopBrainDaemon(),execSync11(`rm -rf "${BRAIN_DIR2}"`,{stdio:"pipe"}),console.log(" Brain uninstalled.")}catch{console.error(` Uninstall failed. Manual: rm -rf ${BRAIN_DIR2}`)}}function isModuleNotFound(msg){return msg.includes("Cannot find")||msg.includes("not found")||msg.includes("MODULE_NOT_FOUND")}function printNotInstalledMessage(){console.log(""),console.log(" Brain is an enterprise knowledge graph engine."),console.log(" It is not installed."),console.log(""),console.log(" Quick install:"),console.log(""),console.log(" genie brain install"),console.log(""),console.log(" Requires GitHub org membership (khal-os)."),console.log("")}async function executeBrainCommand(args){try{let brain=await import(BRAIN_PKG2);if(brain.execute){await brain.execute(args);let check2=checkForUpdates();if(check2.updateAvailable&&check2.latestVersion)console.log(`
|
|
3028
|
+
Update available (${check2.latestVersion}). Run: genie brain upgrade`)}else console.error("Brain module loaded but execute() not found."),console.error("Update: genie brain install")}catch(err){let msg=err instanceof Error?err.message:String(err);if(isModuleNotFound(msg))printNotInstalledMessage();else console.error(`Brain error: ${msg}`)}}function registerBrainCommands(program2){let brain=program2.command("brain").description("Knowledge graph engine (enterprise)").allowUnknownOption().allowExcessArguments().action(async(_options,cmd)=>{let args=cmd.args;if(args.length===0){brain.help();return}await executeBrainCommand(args)});brain.command("install").description("Install genie-brain from GitHub").action(async()=>{await installBrain()}),brain.command("uninstall").description("Remove genie-brain installation").action(async()=>{await uninstallBrain()}),brain.command("upgrade").description("Upgrade genie-brain to latest version").action(async()=>{await updateBrain()}),brain.command("version").description("Show installed brain version").action(async()=>{await showVersion()})}var _brief2;async function getBrief2(){if(!_brief2)_brief2=await Promise.resolve().then(() => (init_brief(),exports_brief));return _brief2}async function handleBrief2(options){let team=options.team??process.env.GENIE_TEAM;if(!team)console.error("Error: --team is required (or set GENIE_TEAM)"),process.exit(1);let agent=options.agent??process.env.GENIE_AGENT_NAME,briefService=await getBrief2(),brief=await briefService.generateBrief({team,agent,since:options.since,repoPath:process.cwd()});console.log(briefService.formatBrief(brief))}function registerBriefCommands(program2){program2.command("brief").description("Show startup brief \u2014 aggregated context since last session").option("--team <name>","Team name (default: GENIE_TEAM)").option("--agent <name>","Agent name (default: GENIE_AGENT_NAME)").option("--since <iso>","Start timestamp (default: last executor end)").action(async(options)=>{try{await handleBrief2(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}import{existsSync as existsSync30,mkdirSync as mkdirSync14,readFileSync as readFileSync19,unlinkSync as unlinkSync8,writeFileSync as writeFileSync17}from"fs";import{homedir as homedir26}from"os";import{join as join39}from"path";function genieHome4(){return process.env.GENIE_HOME??join39(homedir26(),".genie")}function pidFilePath(){return join39(genieHome4(),"scheduler.pid")}function logFilePath(){return join39(genieHome4(),"logs","scheduler.log")}function systemdDir(){return join39(homedir26(),".config","systemd","user")}function systemdUnitPath(){return join39(systemdDir(),"genie-scheduler.service")}function readPid(){let path3=pidFilePath();if(!existsSync30(path3))return null;let raw=readFileSync19(path3,"utf-8").trim(),pid=Number.parseInt(raw,10);if(Number.isNaN(pid)||pid<=0)return null;return pid}function removePid(){let path3=pidFilePath();if(existsSync30(path3))try{unlinkSync8(path3)}catch{}}function isProcessAlive3(pid){try{return process.kill(pid,0),!0}catch{return!1}}function servePidPath2(){return join39(genieHome4(),"serve.pid")}function readServePid2(){let path3=servePidPath2();if(!existsSync30(path3))return null;let raw=readFileSync19(path3,"utf-8").trim(),pid=Number.parseInt(raw,10);if(Number.isNaN(pid)||pid<=0)return null;return pid}function removeServePid2(){let path3=servePidPath2();if(existsSync30(path3))try{unlinkSync8(path3)}catch{}}function generateSystemdUnit(){let genieBin=process.argv[1]??"genie";return`[Unit]
|
|
3029
3029
|
Description=Genie Serve (headless) \u2014 pgserve + scheduler + services
|
|
3030
3030
|
Documentation=https://github.com/automagik/genie
|
|
3031
3031
|
After=network.target
|
|
@@ -3048,7 +3048,7 @@ WantedBy=default.target
|
|
|
3048
3048
|
`}async function daemonInstallCommand(){let unitContent=generateSystemdUnit(),unitPath=systemdUnitPath();mkdirSync14(systemdDir(),{recursive:!0}),writeFileSync17(unitPath,unitContent,"utf-8"),console.log(`Wrote systemd unit: ${unitPath}`);let{spawnSync:spawnSync5}=await import("child_process"),reloadResult=spawnSync5("systemctl",["--user","daemon-reload"],{stdio:["ignore","pipe","pipe"]});if(reloadResult.status!==0){let stderr=reloadResult.stderr?.toString().trim();if(console.log(`
|
|
3049
3049
|
Note: systemctl daemon-reload failed (systemd may not be available).`),stderr)console.log(` ${stderr}`);console.log("You can still run manually: genie serve --headless --foreground");return}let enableResult=spawnSync5("systemctl",["--user","enable","genie-scheduler.service"],{stdio:["ignore","pipe","pipe"]});if(enableResult.status===0)console.log("Enabled genie-scheduler.service"),console.log(`
|
|
3050
3050
|
To start: systemctl --user start genie-scheduler`),console.log("Or: genie serve --headless");else{let stderr=enableResult.stderr?.toString().trim();if(console.log(`
|
|
3051
|
-
Note: systemctl enable failed.`),stderr)console.log(` ${stderr}`);console.log("You can start manually: genie serve --headless")}}async function daemonStartCommand(options){console.log("Note: `genie daemon start` now redirects to `genie serve --headless`."),console.log(" Use `genie serve --headless` directly in the future.\n");let{spawn:spawnChild}=await import("child_process"),bunPath=process.execPath??"bun",genieBin=process.argv[1]??"genie";if(options.foreground)spawnChild(bunPath,[genieBin,"serve","start","--headless","--foreground"],{stdio:"inherit",env:{...process.env}}).on("exit",(code)=>process.exit(code??0)),await new Promise(()=>{});else spawnChild(bunPath,[genieBin,"serve","start","--headless","--daemon"],{stdio:"inherit",env:{...process.env}}).on("exit",(code)=>process.exit(code??0))}async function daemonStopCommand(){let pid=readServePid2(),source="serve";if(!pid)pid=readPid(),source="scheduler";if(!pid){console.log("No serve or scheduler PID file found. Nothing is running.");return}if(!
|
|
3051
|
+
Note: systemctl enable failed.`),stderr)console.log(` ${stderr}`);console.log("You can start manually: genie serve --headless")}}async function daemonStartCommand(options){console.log("Note: `genie daemon start` now redirects to `genie serve --headless`."),console.log(" Use `genie serve --headless` directly in the future.\n");let{spawn:spawnChild}=await import("child_process"),bunPath=process.execPath??"bun",genieBin=process.argv[1]??"genie";if(options.foreground)spawnChild(bunPath,[genieBin,"serve","start","--headless","--foreground"],{stdio:"inherit",env:{...process.env}}).on("exit",(code)=>process.exit(code??0)),await new Promise(()=>{});else spawnChild(bunPath,[genieBin,"serve","start","--headless","--daemon"],{stdio:"inherit",env:{...process.env}}).on("exit",(code)=>process.exit(code??0))}async function daemonStopCommand(){let pid=readServePid2(),source="serve";if(!pid)pid=readPid(),source="scheduler";if(!pid){console.log("No serve or scheduler PID file found. Nothing is running.");return}if(!isProcessAlive3(pid)){console.log(`Stale PID file (${source}, PID ${pid} is not running). Cleaning up.`),removeServePid2(),removePid();return}console.log(`Stopping genie serve (PID ${pid})...`),process.kill(pid,"SIGTERM");let deadline=Date.now()+1e4;while(Date.now()<deadline&&isProcessAlive3(pid))await new Promise((resolve7)=>setTimeout(resolve7,250));if(isProcessAlive3(pid)){console.log("Did not stop within 10s. Sending SIGKILL.");try{process.kill(pid,"SIGKILL")}catch{}}removeServePid2(),removePid(),console.log("Genie serve stopped.")}function getProcessUptime(pid){try{let procStat=readFileSync19(`/proc/${pid}/stat`,"utf-8"),bootTimeJiffies=Number.parseInt(procStat.split(" ")[21],10);if(Number.isNaN(bootTimeJiffies))return null;let uptimeSec=readFileSync19("/proc/uptime","utf-8"),processUptimeS=Number.parseFloat(uptimeSec.split(" ")[0])-bootTimeJiffies/100;return processUptimeS>0?formatUptime(processUptimeS*1000):null}catch{return null}}async function printDaemonStats(){try{let{getConnection:getConnection2,shutdown:shutdown3}=await Promise.resolve().then(() => (init_db(),exports_db)),sql=await getConnection2(),firedResult=await sql`
|
|
3052
3052
|
SELECT count(*)::int AS cnt FROM triggers WHERE status IN ('executing', 'completed')
|
|
3053
3053
|
`;console.log(` Fired: ${firedResult[0]?.cnt??0} trigger(s)`);let pendingResult=await sql`
|
|
3054
3054
|
SELECT count(*)::int AS cnt FROM triggers WHERE status = 'pending'
|
|
@@ -3059,7 +3059,7 @@ Note: systemctl enable failed.`),stderr)console.log(` ${stderr}`);console.log("
|
|
|
3059
3059
|
WHERE status = 'failed' AND error IS NOT NULL
|
|
3060
3060
|
ORDER BY completed_at DESC
|
|
3061
3061
|
LIMIT 1
|
|
3062
|
-
`;if(lastError.length>0&&lastError[0].error)console.log(` Last err: ${lastError[0].error.slice(0,80)}`);await shutdown3()}catch{console.log(" (database not available \u2014 stats unavailable)")}}async function daemonStatusCommand(){let sPid=readServePid2(),schPid=readPid(),pid=sPid&&
|
|
3062
|
+
`;if(lastError.length>0&&lastError[0].error)console.log(` Last err: ${lastError[0].error.slice(0,80)}`);await shutdown3()}catch{console.log(" (database not available \u2014 stats unavailable)")}}async function daemonStatusCommand(){let sPid=readServePid2(),schPid=readPid(),pid=sPid&&isProcessAlive3(sPid)?sPid:schPid,running2=pid!==null&&isProcessAlive3(pid);if(console.log(`
|
|
3063
3063
|
Genie Scheduler Daemon`),console.log("\u2500".repeat(50)),console.log(` Status: ${running2?"running":"stopped"}`),running2&&pid){console.log(` PID: ${pid}`);let uptime=getProcessUptime(pid);if(uptime)console.log(` Uptime: ${uptime}`)}try{let{getInboxPollIntervalMs:getInboxPollIntervalMs2}=await Promise.resolve().then(() => (init_inbox_watcher(),exports_inbox_watcher)),inboxPollMs=getInboxPollIntervalMs2();if(inboxPollMs===0)console.log(" Inbox: disabled (GENIE_INBOX_POLL_MS=0)");else console.log(` Inbox: ${running2?"watching":"stopped"} (poll every ${inboxPollMs/1000}s)`)}catch{console.log(" Inbox: unavailable")}await printDaemonStats(),console.log(` PID file: ${servePidPath2()} (canonical) / ${pidFilePath()} (legacy)`),console.log(` Log file: ${logFilePath()}`),console.log("")}async function daemonLogsCommand(options){let logPath=logFilePath();if(!existsSync30(logPath)){console.log("No scheduler log file found. Start the daemon first."),console.log(` Expected: ${logPath}`);return}let linesToShow=options.lines??20;if(options.follow)await tailFollow(logPath,linesToShow);else tailStatic(logPath,linesToShow)}function tailStatic(filePath,lines){let allLines=readFileSync19(filePath,"utf-8").trim().split(`
|
|
3064
3064
|
`).filter(Boolean),start2=Math.max(0,allLines.length-lines),slice=allLines.slice(start2);for(let line of slice)printLogLine(line);if(allLines.length>lines)console.log(`
|
|
3065
3065
|
(showing last ${lines} of ${allLines.length} entries)`)}async function tailFollow(filePath,initialLines){let{watch:watch2}=await import("fs");tailStatic(filePath,initialLines),console.log(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.260407.
|
|
3
|
+
"version": "4.260407.7",
|
|
4
4
|
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Namastex Labs"
|
|
@@ -62,6 +62,120 @@ function readLocalBrainVersion(): string | undefined {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// ── Daemon lifecycle helpers ────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/** Resolve the brain CLI binary. Prefers .bin symlink, falls back to dist/cli.js. */
|
|
68
|
+
function resolveBrainBin(): string | undefined {
|
|
69
|
+
const candidates = [join(resolveGenieRoot(), 'node_modules', '.bin', 'brain'), join(BRAIN_DIR, 'dist', 'cli.js')];
|
|
70
|
+
for (const c of candidates) {
|
|
71
|
+
if (existsSync(c)) return c;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Search for a brain vault (brain.json) in common locations. Returns path or null. */
|
|
77
|
+
function findBrainVault(): string | null {
|
|
78
|
+
const candidates = [process.cwd(), join(process.cwd(), 'brain'), join(homedir(), 'brain')];
|
|
79
|
+
for (const dir of candidates) {
|
|
80
|
+
if (existsSync(join(dir, 'brain.json'))) return dir;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface ActiveBrainConfig {
|
|
86
|
+
pid: number;
|
|
87
|
+
pgPort?: number;
|
|
88
|
+
brainPath?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Read ~/.brain/config.json for running server PID + brainPath. Returns null on failure. */
|
|
92
|
+
function readActiveBrainConfig(): ActiveBrainConfig | null {
|
|
93
|
+
try {
|
|
94
|
+
const configPath = join(homedir(), '.brain', 'config.json');
|
|
95
|
+
if (!existsSync(configPath)) return null;
|
|
96
|
+
const data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
97
|
+
if (!data.pid) return null;
|
|
98
|
+
return { pid: data.pid, pgPort: data.pgPort, brainPath: data.brainPath };
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Check if a process is alive by signaling 0. */
|
|
105
|
+
function isProcessAlive(pid: number): boolean {
|
|
106
|
+
try {
|
|
107
|
+
process.kill(pid, 0);
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Stop a running brain daemon gracefully.
|
|
116
|
+
* Calls `brain serve stop --brain-path`, polls PID 200ms × 25 (5s), SIGKILL fallback.
|
|
117
|
+
* Returns true if a daemon was running and stopped.
|
|
118
|
+
*/
|
|
119
|
+
async function stopBrainDaemon(): Promise<boolean> {
|
|
120
|
+
const config = readActiveBrainConfig();
|
|
121
|
+
if (!config?.pid || !isProcessAlive(config.pid)) return false;
|
|
122
|
+
|
|
123
|
+
const brainBin = resolveBrainBin();
|
|
124
|
+
const brainPath = config.brainPath;
|
|
125
|
+
|
|
126
|
+
// Use brain's own stop command
|
|
127
|
+
if (brainBin) {
|
|
128
|
+
try {
|
|
129
|
+
const pathArg = brainPath ? ` --brain-path "${brainPath}"` : '';
|
|
130
|
+
execSync(`"${brainBin}" serve stop${pathArg}`, { stdio: 'pipe', timeout: 10000 });
|
|
131
|
+
} catch {
|
|
132
|
+
// Fall through to PID polling — stop command may not be available in older versions
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Poll PID for up to 5s (25 × 200ms)
|
|
137
|
+
for (let i = 0; i < 25; i++) {
|
|
138
|
+
if (!isProcessAlive(config.pid)) return true;
|
|
139
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// SIGKILL fallback
|
|
143
|
+
try {
|
|
144
|
+
process.kill(config.pid, 'SIGKILL');
|
|
145
|
+
} catch {
|
|
146
|
+
// Already gone
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Start brain daemon for a vault. Logs result. Never throws. */
|
|
153
|
+
function startBrainDaemon(vaultPath: string, extraArgs?: string[]): void {
|
|
154
|
+
const bin = resolveBrainBin();
|
|
155
|
+
if (!bin) return;
|
|
156
|
+
try {
|
|
157
|
+
const argsStr = extraArgs?.length ? ` ${extraArgs.join(' ')}` : '';
|
|
158
|
+
execSync(`"${bin}" serve --daemon --brain-path "${vaultPath}"${argsStr}`, {
|
|
159
|
+
stdio: 'inherit',
|
|
160
|
+
timeout: 15000,
|
|
161
|
+
});
|
|
162
|
+
console.log(' Brain daemon started.');
|
|
163
|
+
} catch {
|
|
164
|
+
console.log(' Daemon failed to start. Run: brain serve --daemon');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Read saved daemon args from .brain-server.json in a vault. */
|
|
169
|
+
function readSavedDaemonArgs(brainPath: string): string[] | undefined {
|
|
170
|
+
try {
|
|
171
|
+
const serverJsonPath = join(brainPath, '.brain-server.json');
|
|
172
|
+
const serverInfo = JSON.parse(readFileSync(serverJsonPath, 'utf-8'));
|
|
173
|
+
return serverInfo.args;
|
|
174
|
+
} catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
65
179
|
// ── Cache-only update check (no network, sync, never throws) ──────────────
|
|
66
180
|
|
|
67
181
|
interface UpdateCheck {
|
|
@@ -160,6 +274,11 @@ async function updateBrain(): Promise<boolean> {
|
|
|
160
274
|
console.log(` Upgrading: ${oldVersion} → ${newVersion}`);
|
|
161
275
|
console.log('');
|
|
162
276
|
|
|
277
|
+
// Stop running daemon before upgrade (read saved args for restart)
|
|
278
|
+
const activeConfig = readActiveBrainConfig();
|
|
279
|
+
const savedArgs = activeConfig?.brainPath ? readSavedDaemonArgs(activeConfig.brainPath) : undefined;
|
|
280
|
+
const wasRunning = activeConfig ? await stopBrainDaemon() : false;
|
|
281
|
+
|
|
163
282
|
// Download and extract new tarball (same flow as install)
|
|
164
283
|
const tmpDir = join(homedir(), '.cache', 'genie-brain');
|
|
165
284
|
mkdirSync(tmpDir, { recursive: true });
|
|
@@ -194,6 +313,11 @@ async function updateBrain(): Promise<boolean> {
|
|
|
194
313
|
// Refresh version cache
|
|
195
314
|
refreshVersionCache(newVersion);
|
|
196
315
|
|
|
316
|
+
// Restart daemon if it was running before upgrade
|
|
317
|
+
if (wasRunning && activeConfig?.brainPath) {
|
|
318
|
+
startBrainDaemon(activeConfig.brainPath, savedArgs);
|
|
319
|
+
}
|
|
320
|
+
|
|
197
321
|
return true;
|
|
198
322
|
}
|
|
199
323
|
|
|
@@ -296,6 +420,14 @@ async function installBrain(): Promise<boolean> {
|
|
|
296
420
|
console.log(' Auto-migration skipped. Run: genie brain migrate');
|
|
297
421
|
}
|
|
298
422
|
|
|
423
|
+
// Auto-start daemon if a vault is found
|
|
424
|
+
const vaultPath = findBrainVault();
|
|
425
|
+
if (vaultPath) {
|
|
426
|
+
startBrainDaemon(vaultPath);
|
|
427
|
+
} else {
|
|
428
|
+
console.log(' No brain vault found. Create one with: brain init --name <name> --path <path>');
|
|
429
|
+
}
|
|
430
|
+
|
|
299
431
|
console.log('');
|
|
300
432
|
console.log(' Get started:');
|
|
301
433
|
console.log(' genie brain init --name my-brain --path ./brain');
|
|
@@ -327,12 +459,16 @@ async function installBrain(): Promise<boolean> {
|
|
|
327
459
|
}
|
|
328
460
|
}
|
|
329
461
|
|
|
330
|
-
function uninstallBrain(): void {
|
|
462
|
+
async function uninstallBrain(): Promise<void> {
|
|
331
463
|
try {
|
|
332
464
|
if (!existsSync(BRAIN_DIR)) {
|
|
333
465
|
console.log(' Brain is not installed.');
|
|
334
466
|
return;
|
|
335
467
|
}
|
|
468
|
+
|
|
469
|
+
// Stop running daemon before removing files
|
|
470
|
+
await stopBrainDaemon();
|
|
471
|
+
|
|
336
472
|
execSync(`rm -rf "${BRAIN_DIR}"`, { stdio: 'pipe' });
|
|
337
473
|
console.log(' Brain uninstalled.');
|
|
338
474
|
} catch {
|
|
@@ -409,8 +545,8 @@ export function registerBrainCommands(program: Command): void {
|
|
|
409
545
|
brain
|
|
410
546
|
.command('uninstall')
|
|
411
547
|
.description('Remove genie-brain installation')
|
|
412
|
-
.action(() => {
|
|
413
|
-
uninstallBrain();
|
|
548
|
+
.action(async () => {
|
|
549
|
+
await uninstallBrain();
|
|
414
550
|
});
|
|
415
551
|
|
|
416
552
|
brain
|