@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.
@@ -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
- // Pass template_vars as ?vars= so HQ expands {{VAR_NAME}} in SKILL.md
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 varsParam = template_vars
136
- ? `?vars=${Buffer.from(JSON.stringify(template_vars)).toString('base64')}`
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
- // Sync to process.env so running child processes inherit the updated value
102
- process.env[key] = value
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
- // Sync to process.env so running child processes no longer inherit the removed value
119
- delete process.env[key]
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 a merged environment object from minion variables and secrets.
135
- * Used by workflow/routine runners to inject into execution environment.
136
- * Secrets override variables when keys collide.
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
- * @param {Record<string, string>} [extraVars] - Additional variables (e.g. project/workflow vars from HQ)
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(extraVars = {}) {
142
- const variables = getAll('variables')
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 = {
@@ -544,11 +544,20 @@ Response:
544
544
  }
545
545
  ```
546
546
 
547
- ワークフロー変数はプロジェクト変数を上書きする(同名キーの場合)。実行時のマージ順序:
548
- 1. ミニオンローカル変数
549
- 2. ミニオンシークレット
550
- 3. プロジェクト変数
551
- 4. ワークフロー変数(最優先)
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
 
@@ -49,7 +49,7 @@ Use {{PROJECT_VAR}} to reference project/workflow variables.
49
49
 
50
50
  ### テンプレート変数
51
51
 
52
- スキル本文中で `{{VAR_NAME}}` と記述すると、ワークフロー実行時にHQ上のプロジェクト変数・ワークフロー変数の値で自動的に置換される。スキルを再利用しつつ、プロジェクトごとに異なるパラメータを渡したい場合に使う。
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
- - ミニオン変数・シークレットは `process.env` 経由で利用可能(テンプレートではなく環境変数として)
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
 
@@ -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 variables/secrets are already set in
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 variables/secrets into process.env for child process inheritance
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 variables/secrets into process.env`)
312
+ console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion secrets into process.env`)
312
313
 
313
314
  // Sync bundled assets
314
315
  syncBundledRules()
@@ -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 variables/secrets are already set in
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "3.5.34",
3
+ "version": "3.6.1",
4
4
  "description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
5
5
  "main": "linux/server.js",
6
6
  "bin": {
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
- ミニオン変数・シークレット(HQ UIまたはAPI経由で設定)はサーバー起動時に `process.env` にロードされ、全子プロセスで利用可能。
136
+ **変数**(ミニオン変数・プロジェクト変数・ワークフロー変数)はスキル本文の `{{VAR_NAME}}` テンプレートとして実行時に展開される。スキル作成時にパラメータ化したい値は `{{変数名}}` で記述すること。展開優先順位: ミニオン変数 < プロジェクト変数 < ワークフロー変数(後者が優先)。
137
137
 
138
- プロジェクト変数・ワークフロー変数はスキル本文の `{{VAR_NAME}}` テンプレートとして展開される。スキル作成時にパラメータ化したい値は `{{変数名}}` で記述すること。
138
+ **シークレット**(ミニオンシークレット)はサーバー起動時に `process.env` にロードされ、全子プロセスで環境変数 `$SECRET_NAME` として利用可能。APIキーやパスワード等の機密情報に使用する。シークレットは `{{VAR}}` テンプレートでは展開されない。
139
+
140
+ デイリーログやメモリーから変数・シークレットの値を推測して使用してはならない。変数は `{{VAR_NAME}}` テンプレートとして定義し、シークレットは環境変数として参照すること。
139
141
 
140
142
  ## Skills Directory
141
143
 
@@ -114,25 +114,46 @@ function Test-CommandExists {
114
114
  $null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
115
115
  }
116
116
 
117
- function Invoke-Winget {
118
- # Wrapper for winget install that handles broken sources.
119
- # winget's msstore source can fail with 0x8a15000f on fresh/misconfigured systems.
120
- # This function tries: (1) --source winget, (2) winget source reset + retry, (3) fail.
121
- param([string]$Id)
122
-
123
- if (-not (Test-CommandExists 'winget')) { return $false }
124
-
125
- # Attempt 1: install with --source winget (skip msstore)
126
- $result = cmd /c "winget install --id $Id --source winget --accept-package-agreements --accept-source-agreements 2>&1"
127
- if ($LASTEXITCODE -eq 0) { return $true }
128
-
129
- # Attempt 2: reset sources and retry
130
- Write-Host " Retrying after winget source reset..."
131
- cmd /c "winget source reset --force 2>&1" | Out-Null
132
- $result = cmd /c "winget install --id $Id --source winget --accept-package-agreements --accept-source-agreements 2>&1"
133
- if ($LASTEXITCODE -eq 0) { return $true }
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
- return $false
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-Host " Installing Node.js via winget..."
523
- $installed = Invoke-Winget 'OpenJS.NodeJS.LTS'
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
- Write-Host " Installing Git via winget..."
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
- Write-Host " Python not found. Installing via winget..."
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: winget install --id Python.Python.3.12"
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 " Installing cloudflared..."
1274
- if (Test-CommandExists 'winget') {
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
- Write-Warn "minion-cloudflared service not registered. Run 'setup' as admin to register tunnel service."
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"
@@ -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 variables/secrets are already set in
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 variables/secrets into process.env for child process inheritance
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 variables/secrets into process.env`)
259
+ console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion secrets into process.env`)
259
260
 
260
261
  // Sync bundled assets
261
262
  syncBundledRules()
@@ -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 variables/secrets are already set in
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