@geekbeer/minion 3.5.34 → 3.6.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/core/lib/step-poller.js +7 -3
- package/core/lib/template-expander.js +85 -0
- package/core/stores/variable-store.js +16 -14
- package/docs/api-reference.md +14 -5
- package/docs/task-guides.md +14 -3
- package/linux/routine-runner.js +18 -1
- package/linux/server.js +3 -2
- package/linux/workflow-runner.js +18 -1
- package/package.json +1 -1
- package/rules/core.md +4 -2
- package/win/minion-cli.ps1 +75 -55
- package/win/routine-runner.js +18 -1
- package/win/server.js +3 -2
- package/win/workflow-runner.js +18 -1
package/core/lib/step-poller.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
const { config, isHqConfigured } = require('../config')
|
|
22
22
|
const api = require('../api')
|
|
23
|
+
const variableStore = require('../stores/variable-store')
|
|
23
24
|
|
|
24
25
|
// Polling interval: 30 seconds (matches heartbeat frequency)
|
|
25
26
|
const POLL_INTERVAL_MS = 30_000
|
|
@@ -129,11 +130,14 @@ async function executeStep(step) {
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
// 2. Fetch the skill from HQ to ensure it's deployed locally
|
|
132
|
-
//
|
|
133
|
+
// Merge minion variables into template_vars so HQ expands {{VAR_NAME}} in SKILL.md.
|
|
134
|
+
// Merge order: minion vars < project vars < workflow vars (template_vars already merged by HQ)
|
|
133
135
|
if (skill_name) {
|
|
134
136
|
try {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
+
const minionVars = variableStore.getAll('variables')
|
|
138
|
+
const mergedVars = { ...minionVars, ...(template_vars || {}) }
|
|
139
|
+
const varsParam = Object.keys(mergedVars).length > 0
|
|
140
|
+
? `?vars=${Buffer.from(JSON.stringify(mergedVars)).toString('base64')}`
|
|
137
141
|
: ''
|
|
138
142
|
const fetchUrl = `http://localhost:${config.AGENT_PORT || 8080}/api/skills/fetch/${encodeURIComponent(skill_name)}${varsParam}`
|
|
139
143
|
const fetchResp = await fetch(fetchUrl, {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Expander
|
|
3
|
+
*
|
|
4
|
+
* Expands {{VAR_NAME}} placeholders in SKILL.md files using minion variables.
|
|
5
|
+
* Same syntax as project/workflow variable expansion done on HQ side.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Variables (non-sensitive config) use {{VAR}} template expansion in skill content
|
|
9
|
+
* - Secrets (sensitive credentials) remain as environment variables ($ENV_VAR)
|
|
10
|
+
* - Expand at execution time (not write time) so variable changes take effect immediately
|
|
11
|
+
* - Save/restore original SKILL.md to avoid persisting expanded content
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs').promises
|
|
15
|
+
const path = require('path')
|
|
16
|
+
const { config } = require('../config')
|
|
17
|
+
const variableStore = require('../stores/variable-store')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Expand {{VAR_NAME}} template placeholders in content string.
|
|
21
|
+
* Unmatched placeholders are left as-is (same behavior as HQ-side expansion).
|
|
22
|
+
*
|
|
23
|
+
* @param {string} content - Skill content with {{VAR}} placeholders
|
|
24
|
+
* @param {Record<string, string>} vars - Key-value pairs for substitution
|
|
25
|
+
* @returns {string} Content with matched placeholders replaced
|
|
26
|
+
*/
|
|
27
|
+
function expandTemplateVars(content, vars) {
|
|
28
|
+
return content.replace(/\{\{(\w+)\}\}/g, (match, key) =>
|
|
29
|
+
vars[key] !== undefined ? vars[key] : match
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Expand templates in SKILL.md files for the given skill names.
|
|
35
|
+
* Reads each SKILL.md, expands {{VAR}} with minion variables, writes back.
|
|
36
|
+
* Returns the original contents for restoration after execution.
|
|
37
|
+
*
|
|
38
|
+
* @param {string[]} skillNames - Skill slugs to expand
|
|
39
|
+
* @returns {Promise<Map<string, string>>} Map of skillName -> original content (for restoration)
|
|
40
|
+
*/
|
|
41
|
+
async function expandSkillTemplates(skillNames) {
|
|
42
|
+
const minionVars = variableStore.getAll('variables')
|
|
43
|
+
|
|
44
|
+
// If no variables defined, skip
|
|
45
|
+
if (Object.keys(minionVars).length === 0) return new Map()
|
|
46
|
+
|
|
47
|
+
const originals = new Map()
|
|
48
|
+
const skillsDir = path.join(config.HOME_DIR, '.claude', 'skills')
|
|
49
|
+
|
|
50
|
+
for (const name of skillNames) {
|
|
51
|
+
const skillMdPath = path.join(skillsDir, name, 'SKILL.md')
|
|
52
|
+
try {
|
|
53
|
+
const original = await fs.readFile(skillMdPath, 'utf-8')
|
|
54
|
+
const expanded = expandTemplateVars(original, minionVars)
|
|
55
|
+
if (expanded !== original) {
|
|
56
|
+
originals.set(name, original)
|
|
57
|
+
await fs.writeFile(skillMdPath, expanded, 'utf-8')
|
|
58
|
+
console.log(`[TemplateExpander] Expanded {{VAR}} in skill: ${name}`)
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(`[TemplateExpander] Failed to expand ${name}: ${err.message}`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return originals
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Restore original SKILL.md contents after execution.
|
|
70
|
+
*
|
|
71
|
+
* @param {Map<string, string>} originals - Map from expandSkillTemplates
|
|
72
|
+
*/
|
|
73
|
+
async function restoreSkillTemplates(originals) {
|
|
74
|
+
const skillsDir = path.join(config.HOME_DIR, '.claude', 'skills')
|
|
75
|
+
for (const [name, content] of originals) {
|
|
76
|
+
const skillMdPath = path.join(skillsDir, name, 'SKILL.md')
|
|
77
|
+
try {
|
|
78
|
+
await fs.writeFile(skillMdPath, content, 'utf-8')
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error(`[TemplateExpander] Failed to restore ${name}: ${err.message}`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { expandTemplateVars, expandSkillTemplates, restoreSkillTemplates }
|
|
@@ -89,6 +89,8 @@ function get(type, key) {
|
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* Set a key-value pair (creates or updates).
|
|
92
|
+
* Only secrets are synced to process.env (for child process inheritance).
|
|
93
|
+
* Variables use {{VAR}} template expansion in skill content instead.
|
|
92
94
|
* @param {'secrets' | 'variables'} type
|
|
93
95
|
* @param {string} key
|
|
94
96
|
* @param {string} value
|
|
@@ -98,8 +100,10 @@ function set(type, key, value) {
|
|
|
98
100
|
const data = parseEnvFile(filePath)
|
|
99
101
|
data[key] = value
|
|
100
102
|
writeEnvFile(filePath, data)
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
+
// Only sync secrets to process.env; variables use template expansion instead
|
|
104
|
+
if (type === 'secrets') {
|
|
105
|
+
process.env[key] = value
|
|
106
|
+
}
|
|
103
107
|
console.log(`[VariableStore] Set ${type} key: ${key}`)
|
|
104
108
|
}
|
|
105
109
|
|
|
@@ -115,8 +119,10 @@ function remove(type, key) {
|
|
|
115
119
|
if (!(key in data)) return false
|
|
116
120
|
delete data[key]
|
|
117
121
|
writeEnvFile(filePath, data)
|
|
118
|
-
//
|
|
119
|
-
|
|
122
|
+
// Only sync secrets to process.env; variables use template expansion instead
|
|
123
|
+
if (type === 'secrets') {
|
|
124
|
+
delete process.env[key]
|
|
125
|
+
}
|
|
120
126
|
console.log(`[VariableStore] Removed ${type} key: ${key}`)
|
|
121
127
|
return true
|
|
122
128
|
}
|
|
@@ -131,18 +137,14 @@ function listKeys(type) {
|
|
|
131
137
|
}
|
|
132
138
|
|
|
133
139
|
/**
|
|
134
|
-
* Build
|
|
135
|
-
*
|
|
136
|
-
*
|
|
140
|
+
* Build environment object from minion secrets only.
|
|
141
|
+
* Variables are no longer injected as env vars; they use {{VAR}} template
|
|
142
|
+
* expansion in skill content (same mechanism as project/workflow variables).
|
|
137
143
|
*
|
|
138
|
-
* @
|
|
139
|
-
* @returns {Record<string, string>} Merged key-value pairs
|
|
144
|
+
* @returns {Record<string, string>} Secret key-value pairs for process.env
|
|
140
145
|
*/
|
|
141
|
-
function buildEnv(
|
|
142
|
-
|
|
143
|
-
const secrets = getAll('secrets')
|
|
144
|
-
// Merge order: variables < secrets < extraVars (later wins)
|
|
145
|
-
return { ...variables, ...secrets, ...extraVars }
|
|
146
|
+
function buildEnv() {
|
|
147
|
+
return getAll('secrets')
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
module.exports = {
|
package/docs/api-reference.md
CHANGED
|
@@ -544,11 +544,20 @@ Response:
|
|
|
544
544
|
}
|
|
545
545
|
```
|
|
546
546
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
547
|
+
ワークフロー変数はプロジェクト変数を上書きする(同名キーの場合)。
|
|
548
|
+
|
|
549
|
+
#### 変数とシークレットの違い
|
|
550
|
+
|
|
551
|
+
**変数**(ミニオン変数・プロジェクト変数・ワークフロー変数)はスキル本文の `{{VAR_NAME}}` テンプレートとして実行時に展開される。スキル作成時にパラメータ化したい値は `{{変数名}}` で記述する。
|
|
552
|
+
|
|
553
|
+
**シークレット**(ミニオンシークレット)は環境変数 `$SECRET_NAME` としてプロセスに注入される。APIキーやパスワード等の機密情報に使用する。テンプレート展開は行われない。
|
|
554
|
+
|
|
555
|
+
#### テンプレート変数の展開優先順位
|
|
556
|
+
|
|
557
|
+
同名の変数が複数のスコープで定義されている場合、以下の順序で上書きされる:
|
|
558
|
+
1. ミニオン変数(最低優先)
|
|
559
|
+
2. プロジェクト変数
|
|
560
|
+
3. ワークフロー変数(最優先)
|
|
552
561
|
|
|
553
562
|
### Workflows (project-scoped, versioned)
|
|
554
563
|
|
package/docs/task-guides.md
CHANGED
|
@@ -49,7 +49,7 @@ Use {{PROJECT_VAR}} to reference project/workflow variables.
|
|
|
49
49
|
|
|
50
50
|
### テンプレート変数
|
|
51
51
|
|
|
52
|
-
スキル本文中で `{{VAR_NAME}}`
|
|
52
|
+
スキル本文中で `{{VAR_NAME}}` と記述すると、実行時に変数の値で自動的に置換される。スキルを再利用しつつ、スコープごとに異なるパラメータを渡したい場合に使う。
|
|
53
53
|
|
|
54
54
|
```markdown
|
|
55
55
|
---
|
|
@@ -62,8 +62,19 @@ description: サイトをデプロイする
|
|
|
62
62
|
|
|
63
63
|
- 変数名は英数字とアンダースコアのみ(`\w+`)
|
|
64
64
|
- 未定義の変数は `{{VAR_NAME}}` のまま残る(エラーにはならない)
|
|
65
|
-
-
|
|
66
|
-
-
|
|
65
|
+
- 展開優先順位: ミニオン変数 < プロジェクト変数 < ワークフロー変数(後者が上書き)
|
|
66
|
+
- ルーティン実行時もミニオン変数による `{{VAR}}` 展開が行われる
|
|
67
|
+
|
|
68
|
+
### 変数とシークレットの使い分け
|
|
69
|
+
|
|
70
|
+
| 種別 | 構文 | 用途 | 例 |
|
|
71
|
+
|------|------|------|-----|
|
|
72
|
+
| 変数 | `{{VAR_NAME}}` | 設定・パラメータ(非機密) | デプロイ先、サイトURL、プロジェクト名 |
|
|
73
|
+
| シークレット | `$SECRET_NAME`(環境変数) | 機密情報 | APIキー、パスワード、トークン |
|
|
74
|
+
|
|
75
|
+
- **変数**はスキル本文のテンプレートとして展開される。全スコープ(ミニオン・プロジェクト・ワークフロー)で同じ `{{VAR}}` 構文を使用する
|
|
76
|
+
- **シークレット**は環境変数としてプロセスに注入される。テンプレート展開は行われない
|
|
77
|
+
- デイリーログやメモリーから変数・シークレットの値を推測して使用しないこと
|
|
67
78
|
|
|
68
79
|
### 2. HQ に反映する
|
|
69
80
|
|
package/linux/routine-runner.js
CHANGED
|
@@ -22,6 +22,7 @@ const executionStore = require('../core/stores/execution-store')
|
|
|
22
22
|
const routineStore = require('../core/stores/routine-store')
|
|
23
23
|
const logManager = require('../core/lib/log-manager')
|
|
24
24
|
const runningTasks = require('../core/lib/running-tasks')
|
|
25
|
+
const { expandSkillTemplates, restoreSkillTemplates } = require('../core/lib/template-expander')
|
|
25
26
|
|
|
26
27
|
// Active cron jobs keyed by routine ID
|
|
27
28
|
const activeJobs = new Map()
|
|
@@ -83,6 +84,17 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
83
84
|
console.log(`[RoutineRunner] tmux session: ${sessionName}`)
|
|
84
85
|
console.log(`[RoutineRunner] Log file: ${logFile}`)
|
|
85
86
|
|
|
87
|
+
// Expand {{VAR}} templates in SKILL.md files with minion variables
|
|
88
|
+
let expandedOriginals = new Map()
|
|
89
|
+
try {
|
|
90
|
+
expandedOriginals = await expandSkillTemplates(skillNames)
|
|
91
|
+
if (expandedOriginals.size > 0) {
|
|
92
|
+
console.log(`[RoutineRunner] Expanded templates in ${expandedOriginals.size} skill(s)`)
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(`[RoutineRunner] Template expansion failed: ${err.message}`)
|
|
96
|
+
}
|
|
97
|
+
|
|
86
98
|
try {
|
|
87
99
|
// Ensure log directory exists and prune old logs
|
|
88
100
|
await logManager.ensureLogDir()
|
|
@@ -102,7 +114,7 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
102
114
|
const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
|
|
103
115
|
|
|
104
116
|
// Create tmux session with the LLM command.
|
|
105
|
-
// PATH, HOME, DISPLAY, and minion
|
|
117
|
+
// PATH, HOME, DISPLAY, and minion secrets are already set in
|
|
106
118
|
// process.env at server startup, so child processes inherit them automatically.
|
|
107
119
|
// Per-execution identifiers are passed via -e flags for the session environment.
|
|
108
120
|
const tmuxCommand = [
|
|
@@ -173,6 +185,11 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
173
185
|
} catch (error) {
|
|
174
186
|
console.error(`[RoutineRunner] Routine ${routine.name} failed: ${error.message}`)
|
|
175
187
|
return { success: false, error: error.message, sessionName }
|
|
188
|
+
} finally {
|
|
189
|
+
// Restore original SKILL.md files after execution
|
|
190
|
+
if (expandedOriginals.size > 0) {
|
|
191
|
+
await restoreSkillTemplates(expandedOriginals)
|
|
192
|
+
}
|
|
176
193
|
}
|
|
177
194
|
}
|
|
178
195
|
|
package/linux/server.js
CHANGED
|
@@ -302,13 +302,14 @@ async function start() {
|
|
|
302
302
|
process.env.HOME = config.HOME_DIR
|
|
303
303
|
process.env.DISPLAY = process.env.DISPLAY || ':99'
|
|
304
304
|
|
|
305
|
-
// Load minion
|
|
305
|
+
// Load minion secrets into process.env for child process inheritance.
|
|
306
|
+
// Variables are NOT loaded here — they use {{VAR}} template expansion in skill content.
|
|
306
307
|
const variableStore = require('../core/stores/variable-store')
|
|
307
308
|
const minionEnv = variableStore.buildEnv()
|
|
308
309
|
for (const [key, value] of Object.entries(minionEnv)) {
|
|
309
310
|
if (!(key in process.env)) process.env[key] = value
|
|
310
311
|
}
|
|
311
|
-
console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion
|
|
312
|
+
console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion secrets into process.env`)
|
|
312
313
|
|
|
313
314
|
// Sync bundled assets
|
|
314
315
|
syncBundledRules()
|
package/linux/workflow-runner.js
CHANGED
|
@@ -21,6 +21,7 @@ const executionStore = require('../core/stores/execution-store')
|
|
|
21
21
|
const workflowStore = require('../core/stores/workflow-store')
|
|
22
22
|
const logManager = require('../core/lib/log-manager')
|
|
23
23
|
const runningTasks = require('../core/lib/running-tasks')
|
|
24
|
+
const { expandSkillTemplates, restoreSkillTemplates } = require('../core/lib/template-expander')
|
|
24
25
|
|
|
25
26
|
// Active cron jobs keyed by workflow ID
|
|
26
27
|
const activeJobs = new Map()
|
|
@@ -87,6 +88,17 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
87
88
|
console.log(`[WorkflowRunner] Log file: ${logFile}`)
|
|
88
89
|
console.log(`[WorkflowRunner] HOME: ${homeDir}`)
|
|
89
90
|
|
|
91
|
+
// Expand {{VAR}} templates in SKILL.md files with minion variables
|
|
92
|
+
let expandedOriginals = new Map()
|
|
93
|
+
try {
|
|
94
|
+
expandedOriginals = await expandSkillTemplates(skillNames)
|
|
95
|
+
if (expandedOriginals.size > 0) {
|
|
96
|
+
console.log(`[WorkflowRunner] Expanded templates in ${expandedOriginals.size} skill(s)`)
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(`[WorkflowRunner] Template expansion failed: ${err.message}`)
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
try {
|
|
91
103
|
// Ensure log directory exists and prune old logs
|
|
92
104
|
await logManager.ensureLogDir()
|
|
@@ -106,7 +118,7 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
106
118
|
const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
|
|
107
119
|
|
|
108
120
|
// Create tmux session with the LLM command.
|
|
109
|
-
// PATH, HOME, DISPLAY, and minion
|
|
121
|
+
// PATH, HOME, DISPLAY, and minion secrets are already set in
|
|
110
122
|
// process.env at server startup, so child processes inherit them automatically.
|
|
111
123
|
const tmuxCommand = [
|
|
112
124
|
'tmux new-session -d',
|
|
@@ -173,6 +185,11 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
173
185
|
} catch (error) {
|
|
174
186
|
console.error(`[WorkflowRunner] Workflow ${workflow.name} failed: ${error.message}`)
|
|
175
187
|
return { success: false, error: error.message, sessionName }
|
|
188
|
+
} finally {
|
|
189
|
+
// Restore original SKILL.md files after execution
|
|
190
|
+
if (expandedOriginals.size > 0) {
|
|
191
|
+
await restoreSkillTemplates(expandedOriginals)
|
|
192
|
+
}
|
|
176
193
|
}
|
|
177
194
|
}
|
|
178
195
|
|
package/package.json
CHANGED
package/rules/core.md
CHANGED
|
@@ -133,9 +133,11 @@ Routine 実行中は以下もtmuxセッション環境で利用可能:
|
|
|
133
133
|
- `MINION_ROUTINE_ID` — ルーティンUUID
|
|
134
134
|
- `MINION_ROUTINE_NAME` — ルーティン名
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
**変数**(ミニオン変数・プロジェクト変数・ワークフロー変数)はスキル本文の `{{VAR_NAME}}` テンプレートとして実行時に展開される。スキル作成時にパラメータ化したい値は `{{変数名}}` で記述すること。展開優先順位: ミニオン変数 < プロジェクト変数 < ワークフロー変数(後者が優先)。
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
**シークレット**(ミニオンシークレット)はサーバー起動時に `process.env` にロードされ、全子プロセスで環境変数 `$SECRET_NAME` として利用可能。APIキーやパスワード等の機密情報に使用する。シークレットは `{{VAR}}` テンプレートでは展開されない。
|
|
139
|
+
|
|
140
|
+
デイリーログやメモリーから変数・シークレットの値を推測して使用してはならない。変数は `{{VAR_NAME}}` テンプレートとして定義し、シークレットは環境変数として参照すること。
|
|
139
141
|
|
|
140
142
|
## Skills Directory
|
|
141
143
|
|
package/win/minion-cli.ps1
CHANGED
|
@@ -114,25 +114,46 @@ function Test-CommandExists {
|
|
|
114
114
|
$null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
function
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
117
|
+
function Install-GitDirect {
|
|
118
|
+
# Download and install Git for Windows from GitHub Releases.
|
|
119
|
+
# Uses the GitHub API to resolve the latest 64-bit installer URL.
|
|
120
|
+
Write-Host " Downloading Git for Windows..."
|
|
121
|
+
try {
|
|
122
|
+
$releaseApi = 'https://api.github.com/repos/git-for-windows/git/releases/latest'
|
|
123
|
+
$release = Invoke-RestMethod -Uri $releaseApi -UseBasicParsing
|
|
124
|
+
$asset = $release.assets | Where-Object { $_.name -match '64-bit\.exe$' -and $_.name -notmatch 'portable' } | Select-Object -First 1
|
|
125
|
+
if (-not $asset) { Write-Warn "Could not find Git installer from GitHub releases"; return $false }
|
|
126
|
+
|
|
127
|
+
$installerPath = Join-Path $env:TEMP $asset.name
|
|
128
|
+
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $installerPath -UseBasicParsing
|
|
129
|
+
Write-Host " Running Git installer (silent)..."
|
|
130
|
+
$proc = Start-Process -FilePath $installerPath -ArgumentList '/VERYSILENT', '/NORESTART', '/SP-' -Wait -PassThru
|
|
131
|
+
Remove-Item $installerPath -Force -ErrorAction SilentlyContinue
|
|
132
|
+
return ($proc.ExitCode -eq 0)
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
Write-Warn "Git download failed: $_"
|
|
136
|
+
return $false
|
|
137
|
+
}
|
|
138
|
+
}
|
|
134
139
|
|
|
135
|
-
|
|
140
|
+
function Install-PythonDirect {
|
|
141
|
+
# Download and install Python from python.org.
|
|
142
|
+
# Installs for all users with PATH prepended.
|
|
143
|
+
Write-Host " Downloading Python 3.12..."
|
|
144
|
+
try {
|
|
145
|
+
$pyUrl = 'https://www.python.org/ftp/python/3.12.8/python-3.12.8-amd64.exe'
|
|
146
|
+
$installerPath = Join-Path $env:TEMP 'python-installer.exe'
|
|
147
|
+
Invoke-WebRequest -Uri $pyUrl -OutFile $installerPath -UseBasicParsing
|
|
148
|
+
Write-Host " Running Python installer (silent)..."
|
|
149
|
+
$proc = Start-Process -FilePath $installerPath -ArgumentList '/quiet', 'InstallAllUsers=1', 'PrependPath=1', 'Include_pip=1' -Wait -PassThru
|
|
150
|
+
Remove-Item $installerPath -Force -ErrorAction SilentlyContinue
|
|
151
|
+
return ($proc.ExitCode -eq 0)
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
Write-Warn "Python download failed: $_"
|
|
155
|
+
return $false
|
|
156
|
+
}
|
|
136
157
|
}
|
|
137
158
|
|
|
138
159
|
function Remove-ScheduledTaskSilent {
|
|
@@ -519,23 +540,8 @@ function Invoke-Setup {
|
|
|
519
540
|
Write-Detail "Node.js $nodeVersion already installed"
|
|
520
541
|
}
|
|
521
542
|
else {
|
|
522
|
-
Write-
|
|
523
|
-
|
|
524
|
-
if ($installed) {
|
|
525
|
-
# Refresh PATH
|
|
526
|
-
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')
|
|
527
|
-
if (Test-CommandExists 'node') {
|
|
528
|
-
Write-Detail "Node.js installed successfully"
|
|
529
|
-
}
|
|
530
|
-
else {
|
|
531
|
-
Write-Warn "Node.js installed but not in PATH. Please restart this terminal."
|
|
532
|
-
exit 1
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
else {
|
|
536
|
-
Write-Error "Failed to install Node.js. Please install manually from https://nodejs.org/"
|
|
537
|
-
exit 1
|
|
538
|
-
}
|
|
543
|
+
Write-Error "Node.js is required but not installed. Please install from https://nodejs.org/ and re-run setup."
|
|
544
|
+
exit 1
|
|
539
545
|
}
|
|
540
546
|
|
|
541
547
|
# Step 2: Install Git (required by Claude Code for git-bash)
|
|
@@ -551,8 +557,7 @@ function Invoke-Setup {
|
|
|
551
557
|
if (Test-Path $candidateBash) { $gitBashPath = $candidateBash }
|
|
552
558
|
}
|
|
553
559
|
else {
|
|
554
|
-
|
|
555
|
-
$installed = Invoke-Winget 'Git.Git'
|
|
560
|
+
$installed = Install-GitDirect
|
|
556
561
|
if ($installed) {
|
|
557
562
|
# Refresh PATH
|
|
558
563
|
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')
|
|
@@ -808,14 +813,13 @@ function Invoke-Setup {
|
|
|
808
813
|
if ($pyVer -match '\d+\.\d+') { $pythonUsable = $true }
|
|
809
814
|
}
|
|
810
815
|
if (-not $pythonUsable) {
|
|
811
|
-
|
|
812
|
-
$installed = Invoke-Winget 'Python.Python.3.12'
|
|
816
|
+
$installed = Install-PythonDirect
|
|
813
817
|
if ($installed) {
|
|
814
818
|
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')
|
|
815
819
|
Write-Detail "Python installed"
|
|
816
820
|
}
|
|
817
821
|
else {
|
|
818
|
-
Write-Warn "Failed to install Python. Install manually
|
|
822
|
+
Write-Warn "Failed to install Python. Install manually from https://www.python.org/downloads/"
|
|
819
823
|
}
|
|
820
824
|
}
|
|
821
825
|
|
|
@@ -1270,23 +1274,16 @@ function Invoke-Configure {
|
|
|
1270
1274
|
|
|
1271
1275
|
# Install cloudflared if needed
|
|
1272
1276
|
if (-not (Test-CommandExists 'cloudflared')) {
|
|
1273
|
-
Write-Host "
|
|
1274
|
-
|
|
1275
|
-
$installed = Invoke-Winget 'Cloudflare.cloudflared'
|
|
1276
|
-
if ($installed) {
|
|
1277
|
-
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
elseif (Test-CommandExists 'choco') {
|
|
1281
|
-
& choco install cloudflared -y
|
|
1282
|
-
}
|
|
1283
|
-
else {
|
|
1284
|
-
Write-Host " Downloading cloudflared..."
|
|
1277
|
+
Write-Host " Downloading cloudflared..."
|
|
1278
|
+
try {
|
|
1285
1279
|
$cfUrl = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe'
|
|
1286
1280
|
$cfPath = Join-Path $DataDir 'cloudflared.exe'
|
|
1287
|
-
Invoke-WebRequest -Uri $cfUrl -OutFile $cfPath
|
|
1281
|
+
Invoke-WebRequest -Uri $cfUrl -OutFile $cfPath -UseBasicParsing
|
|
1288
1282
|
Write-Detail "cloudflared downloaded to $cfPath"
|
|
1289
1283
|
}
|
|
1284
|
+
catch {
|
|
1285
|
+
Write-Warn "Failed to download cloudflared: $_"
|
|
1286
|
+
}
|
|
1290
1287
|
}
|
|
1291
1288
|
else {
|
|
1292
1289
|
Write-Detail "cloudflared already installed"
|
|
@@ -1314,10 +1311,33 @@ function Invoke-Configure {
|
|
|
1314
1311
|
$cfExe = Join-Path $DataDir 'cloudflared.exe'
|
|
1315
1312
|
}
|
|
1316
1313
|
if ($cfExe) {
|
|
1317
|
-
# Check if cloudflared service already registered
|
|
1318
1314
|
$cfState = Get-ServiceState 'minion-cloudflared'
|
|
1319
1315
|
if (-not $cfState) {
|
|
1320
|
-
|
|
1316
|
+
# Service not registered — try to register via NSSM (requires admin)
|
|
1317
|
+
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
1318
|
+
if ($isAdmin) {
|
|
1319
|
+
$cfConfigPath = Join-Path $cfConfigDir 'config.yml'
|
|
1320
|
+
Invoke-Nssm stop minion-cloudflared
|
|
1321
|
+
Invoke-Nssm remove minion-cloudflared confirm
|
|
1322
|
+
Invoke-Nssm install minion-cloudflared $cfExe "tunnel run --config `"$cfConfigPath`""
|
|
1323
|
+
Invoke-Nssm set minion-cloudflared Start SERVICE_AUTO_START
|
|
1324
|
+
Invoke-Nssm set minion-cloudflared DisplayName "Minion Cloudflared"
|
|
1325
|
+
Invoke-Nssm set minion-cloudflared Description "Cloudflare Tunnel for Minion"
|
|
1326
|
+
Invoke-Nssm set minion-cloudflared AppRestartDelay 5000
|
|
1327
|
+
Invoke-Nssm set minion-cloudflared AppStdout (Join-Path $LogDir 'cloudflared-stdout.log')
|
|
1328
|
+
Invoke-Nssm set minion-cloudflared AppStderr (Join-Path $LogDir 'cloudflared-stderr.log')
|
|
1329
|
+
Invoke-Nssm set minion-cloudflared AppStdoutCreationDisposition 4
|
|
1330
|
+
Invoke-Nssm set minion-cloudflared AppStderrCreationDisposition 4
|
|
1331
|
+
Invoke-Nssm set minion-cloudflared AppRotateFiles 1
|
|
1332
|
+
Invoke-Nssm set minion-cloudflared AppRotateBytes 10485760
|
|
1333
|
+
$setupUserSid = Get-SetupUserSid
|
|
1334
|
+
Grant-ServiceControlToUser 'minion-cloudflared' $setupUserSid
|
|
1335
|
+
Write-Detail "minion-cloudflared service registered"
|
|
1336
|
+
sc.exe start minion-cloudflared 2>&1 | Out-Null
|
|
1337
|
+
Write-Detail "minion-cloudflared started"
|
|
1338
|
+
} else {
|
|
1339
|
+
Write-Warn "minion-cloudflared service not registered. Run 'configure --setup-tunnel' as admin to register tunnel service."
|
|
1340
|
+
}
|
|
1321
1341
|
} else {
|
|
1322
1342
|
sc.exe start minion-cloudflared 2>&1 | Out-Null
|
|
1323
1343
|
Write-Detail "minion-cloudflared started"
|
package/win/routine-runner.js
CHANGED
|
@@ -17,6 +17,7 @@ const executionStore = require('../core/stores/execution-store')
|
|
|
17
17
|
const routineStore = require('../core/stores/routine-store')
|
|
18
18
|
const logManager = require('../core/lib/log-manager')
|
|
19
19
|
const { activeSessions } = require('./workflow-runner')
|
|
20
|
+
const { expandSkillTemplates, restoreSkillTemplates } = require('../core/lib/template-expander')
|
|
20
21
|
|
|
21
22
|
const activeJobs = new Map()
|
|
22
23
|
const runningExecutions = new Map()
|
|
@@ -60,6 +61,17 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
60
61
|
console.log(`[RoutineRunner] Session: ${sessionName}`)
|
|
61
62
|
console.log(`[RoutineRunner] Log file: ${logFile}`)
|
|
62
63
|
|
|
64
|
+
// Expand {{VAR}} templates in SKILL.md files with minion variables
|
|
65
|
+
let expandedOriginals = new Map()
|
|
66
|
+
try {
|
|
67
|
+
expandedOriginals = await expandSkillTemplates(skillNames)
|
|
68
|
+
if (expandedOriginals.size > 0) {
|
|
69
|
+
console.log(`[RoutineRunner] Expanded templates in ${expandedOriginals.size} skill(s)`)
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error(`[RoutineRunner] Template expansion failed: ${err.message}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
try {
|
|
64
76
|
await logManager.ensureLogDir()
|
|
65
77
|
await logManager.pruneOldLogs()
|
|
@@ -76,7 +88,7 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
76
88
|
const escapedPrompt = prompt.replace(/'/g, "''")
|
|
77
89
|
const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
|
|
78
90
|
|
|
79
|
-
// PATH, HOME, USERPROFILE, and minion
|
|
91
|
+
// PATH, HOME, USERPROFILE, and minion secrets are already set in
|
|
80
92
|
// process.env at server startup. Per-execution identifiers are added here.
|
|
81
93
|
const env = {
|
|
82
94
|
...process.env,
|
|
@@ -147,6 +159,11 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
147
159
|
} catch (error) {
|
|
148
160
|
console.error(`[RoutineRunner] Routine ${routine.name} failed: ${error.message}`)
|
|
149
161
|
return { success: false, error: error.message, sessionName }
|
|
162
|
+
} finally {
|
|
163
|
+
// Restore original SKILL.md files after execution
|
|
164
|
+
if (expandedOriginals.size > 0) {
|
|
165
|
+
await restoreSkillTemplates(expandedOriginals)
|
|
166
|
+
}
|
|
150
167
|
}
|
|
151
168
|
}
|
|
152
169
|
|
package/win/server.js
CHANGED
|
@@ -249,13 +249,14 @@ async function start() {
|
|
|
249
249
|
process.env.HOME = config.HOME_DIR
|
|
250
250
|
process.env.USERPROFILE = config.HOME_DIR
|
|
251
251
|
|
|
252
|
-
// Load minion
|
|
252
|
+
// Load minion secrets into process.env for child process inheritance.
|
|
253
|
+
// Variables are NOT loaded here — they use {{VAR}} template expansion in skill content.
|
|
253
254
|
const variableStore = require('../core/stores/variable-store')
|
|
254
255
|
const minionEnv = variableStore.buildEnv()
|
|
255
256
|
for (const [key, value] of Object.entries(minionEnv)) {
|
|
256
257
|
if (!(key in process.env)) process.env[key] = value
|
|
257
258
|
}
|
|
258
|
-
console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion
|
|
259
|
+
console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion secrets into process.env`)
|
|
259
260
|
|
|
260
261
|
// Sync bundled assets
|
|
261
262
|
syncBundledRules()
|
package/win/workflow-runner.js
CHANGED
|
@@ -23,6 +23,7 @@ const { config } = require('../core/config')
|
|
|
23
23
|
const executionStore = require('../core/stores/execution-store')
|
|
24
24
|
const workflowStore = require('../core/stores/workflow-store')
|
|
25
25
|
const logManager = require('../core/lib/log-manager')
|
|
26
|
+
const { expandSkillTemplates, restoreSkillTemplates } = require('../core/lib/template-expander')
|
|
26
27
|
|
|
27
28
|
// Active cron jobs keyed by workflow ID
|
|
28
29
|
const activeJobs = new Map()
|
|
@@ -92,6 +93,17 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
92
93
|
console.log(`[WorkflowRunner] Log file: ${logFile}`)
|
|
93
94
|
console.log(`[WorkflowRunner] HOME: ${homeDir}`)
|
|
94
95
|
|
|
96
|
+
// Expand {{VAR}} templates in SKILL.md files with minion variables
|
|
97
|
+
let expandedOriginals = new Map()
|
|
98
|
+
try {
|
|
99
|
+
expandedOriginals = await expandSkillTemplates(skillNames)
|
|
100
|
+
if (expandedOriginals.size > 0) {
|
|
101
|
+
console.log(`[WorkflowRunner] Expanded templates in ${expandedOriginals.size} skill(s)`)
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error(`[WorkflowRunner] Template expansion failed: ${err.message}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
95
107
|
try {
|
|
96
108
|
await logManager.ensureLogDir()
|
|
97
109
|
await logManager.pruneOldLogs()
|
|
@@ -110,7 +122,7 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
110
122
|
const escapedPrompt = prompt.replace(/'/g, "''")
|
|
111
123
|
const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
|
|
112
124
|
|
|
113
|
-
// PATH, HOME, USERPROFILE, and minion
|
|
125
|
+
// PATH, HOME, USERPROFILE, and minion secrets are already set in
|
|
114
126
|
// process.env at server startup, so child processes inherit them automatically.
|
|
115
127
|
|
|
116
128
|
// Open log file for streaming writes
|
|
@@ -192,6 +204,11 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
192
204
|
} catch (error) {
|
|
193
205
|
console.error(`[WorkflowRunner] Workflow ${workflow.name} failed: ${error.message}`)
|
|
194
206
|
return { success: false, error: error.message, sessionName }
|
|
207
|
+
} finally {
|
|
208
|
+
// Restore original SKILL.md files after execution
|
|
209
|
+
if (expandedOriginals.size > 0) {
|
|
210
|
+
await restoreSkillTemplates(expandedOriginals)
|
|
211
|
+
}
|
|
195
212
|
}
|
|
196
213
|
}
|
|
197
214
|
|