@geekbeer/minion 2.70.2 → 3.4.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.
@@ -1,105 +1,86 @@
1
1
  /**
2
- * Routine Store
3
- * Persists routine configurations to local JSON file.
2
+ * Routine Store (SQLite)
3
+ * Persists routine configurations to local SQLite database.
4
4
  * Allows minion to continue operating when HQ is offline.
5
5
  */
6
6
 
7
- const fs = require('fs').promises
8
- const path = require('path')
9
-
10
- const { config } = require('../config')
11
- const { DATA_DIR } = require('../lib/platform')
12
-
13
- // Routine file location: DATA_DIR/routines.json (platform-aware)
14
- // or ~/routines.json (fallback)
15
- function getRoutineFilePath() {
16
- try {
17
- require('fs').accessSync(DATA_DIR)
18
- return path.join(DATA_DIR, 'routines.json')
19
- } catch {
20
- return path.join(config.HOME_DIR, 'routines.json')
21
- }
22
- }
23
-
24
- const ROUTINE_FILE = getRoutineFilePath()
7
+ const { getDb } = require('../db')
25
8
 
26
9
  /**
27
- * Load routines from local file
28
- * @returns {Promise<Array>} Array of routine objects
10
+ * Load all routines
11
+ * @returns {Array} Array of routine objects
29
12
  */
30
- async function load() {
31
- try {
32
- const data = await fs.readFile(ROUTINE_FILE, 'utf-8')
33
- return JSON.parse(data)
34
- } catch (err) {
35
- if (err.code === 'ENOENT') {
36
- return []
37
- }
38
- console.error(`[RoutineStore] Failed to load routines: ${err.message}`)
39
- return []
40
- }
13
+ function load() {
14
+ const db = getDb()
15
+ const rows = db.prepare('SELECT data FROM routines ORDER BY name').all()
16
+ return rows.map(r => JSON.parse(r.data))
41
17
  }
42
18
 
43
19
  /**
44
- * Save routines to local file
20
+ * Save routines (replace all)
45
21
  * @param {Array} routines - Array of routine objects
46
22
  */
47
- async function save(routines) {
48
- try {
49
- await fs.writeFile(ROUTINE_FILE, JSON.stringify(routines, null, 2), 'utf-8')
50
- console.log(`[RoutineStore] Saved ${routines.length} routines to ${ROUTINE_FILE}`)
51
- } catch (err) {
52
- console.error(`[RoutineStore] Failed to save routines: ${err.message}`)
53
- throw err
54
- }
23
+ function save(routines) {
24
+ const db = getDb()
25
+ const tx = db.transaction(() => {
26
+ db.prepare('DELETE FROM routines').run()
27
+ const insert = db.prepare('INSERT INTO routines (id, name, data) VALUES (?, ?, ?)')
28
+ for (const r of routines) {
29
+ insert.run(r.id, r.name, JSON.stringify(r))
30
+ }
31
+ })
32
+ tx()
33
+ console.log(`[RoutineStore] Saved ${routines.length} routines`)
55
34
  }
56
35
 
57
36
  /**
58
37
  * Update last_run for a specific routine
59
38
  * @param {string} routineId - Routine UUID
60
39
  */
61
- async function updateLastRun(routineId) {
62
- const routines = await load()
63
- const routine = routines.find(r => String(r.id) === String(routineId))
64
- if (routine) {
65
- routine.last_run = new Date().toISOString()
66
- await save(routines)
67
- }
40
+ function updateLastRun(routineId) {
41
+ const db = getDb()
42
+ const row = db.prepare('SELECT data FROM routines WHERE id = ?').get(String(routineId))
43
+ if (!row) return
44
+
45
+ const routine = JSON.parse(row.data)
46
+ routine.last_run = new Date().toISOString()
47
+ db.prepare('UPDATE routines SET data = ? WHERE id = ?').run(JSON.stringify(routine), String(routineId))
68
48
  }
69
49
 
70
50
  /**
71
51
  * Find a routine by name
72
52
  * @param {string} name - Routine name
73
- * @returns {Promise<object|null>} Routine object or null
53
+ * @returns {object|null} Routine object or null
74
54
  */
75
- async function findByName(name) {
76
- const routines = await load()
77
- return routines.find(r => r.name === name) || null
55
+ function findByName(name) {
56
+ const db = getDb()
57
+ const row = db.prepare('SELECT data FROM routines WHERE name = ?').get(name)
58
+ return row ? JSON.parse(row.data) : null
78
59
  }
79
60
 
80
61
  /**
81
62
  * Upsert a routine from HQ data.
82
63
  * Upserts by ID (routines come from HQ with UUIDs).
83
64
  * @param {object} routineData - { id, name, pipeline_skill_names, content, is_active, cron_expression, last_run, next_run }
84
- * @returns {Promise<Array>} Updated routines array
65
+ * @returns {Array} Updated routines array
85
66
  */
86
- async function upsertFromHQ(routineData) {
87
- const routines = await load()
88
- const index = routines.findIndex(r => String(r.id) === String(routineData.id))
67
+ function upsertFromHQ(routineData) {
68
+ const db = getDb()
69
+ const existing = db.prepare('SELECT data FROM routines WHERE id = ?').get(String(routineData.id))
89
70
 
90
- if (index >= 0) {
91
- // Update from HQ (preserve local-only fields if any)
92
- routines[index] = {
93
- ...routines[index],
94
- name: routineData.name,
95
- pipeline_skill_names: routineData.pipeline_skill_names,
96
- content: routineData.content || '',
97
- context: routineData.context || null,
98
- is_active: routineData.is_active,
99
- cron_expression: routineData.cron_expression || '',
100
- }
71
+ if (existing) {
72
+ const routine = JSON.parse(existing.data)
73
+ routine.name = routineData.name
74
+ routine.pipeline_skill_names = routineData.pipeline_skill_names
75
+ routine.content = routineData.content || ''
76
+ routine.context = routineData.context || null
77
+ routine.is_active = routineData.is_active
78
+ routine.cron_expression = routineData.cron_expression || ''
79
+ db.prepare('UPDATE routines SET name = ?, data = ? WHERE id = ?').run(
80
+ routine.name, JSON.stringify(routine), String(routineData.id)
81
+ )
101
82
  } else {
102
- routines.push({
83
+ const routine = {
103
84
  id: routineData.id,
104
85
  name: routineData.name,
105
86
  pipeline_skill_names: routineData.pipeline_skill_names || [],
@@ -109,11 +90,13 @@ async function upsertFromHQ(routineData) {
109
90
  cron_expression: routineData.cron_expression || '',
110
91
  last_run: routineData.last_run || null,
111
92
  next_run: routineData.next_run || null,
112
- })
93
+ }
94
+ db.prepare('INSERT INTO routines (id, name, data) VALUES (?, ?, ?)').run(
95
+ String(routine.id), routine.name, JSON.stringify(routine)
96
+ )
113
97
  }
114
98
 
115
- await save(routines)
116
- return routines
99
+ return load()
117
100
  }
118
101
 
119
102
  module.exports = { load, save, updateLastRun, findByName, upsertFromHQ }
@@ -1,81 +1,62 @@
1
1
  /**
2
- * Workflow Store
3
- * Persists workflow configurations to local JSON file.
2
+ * Workflow Store (SQLite)
3
+ * Persists workflow configurations to local SQLite database.
4
4
  * Allows minion to continue operating when HQ is offline.
5
5
  */
6
6
 
7
- const fs = require('fs').promises
8
- const path = require('path')
9
-
10
- const { config } = require('../config')
11
- const { DATA_DIR } = require('../lib/platform')
12
-
13
- // Workflow file location: DATA_DIR/workflows.json (platform-aware)
14
- // or ~/workflows.json (fallback)
15
- function getWorkflowFilePath() {
16
- try {
17
- require('fs').accessSync(DATA_DIR)
18
- return path.join(DATA_DIR, 'workflows.json')
19
- } catch {
20
- return path.join(config.HOME_DIR, 'workflows.json')
21
- }
22
- }
23
-
24
- const WORKFLOW_FILE = getWorkflowFilePath()
7
+ const crypto = require('crypto')
8
+ const { getDb } = require('../db')
25
9
 
26
10
  /**
27
- * Load workflows from local file
28
- * @returns {Promise<Array>} Array of workflow objects
11
+ * Load all workflows
12
+ * @returns {Array} Array of workflow objects
29
13
  */
30
- async function load() {
31
- try {
32
- const data = await fs.readFile(WORKFLOW_FILE, 'utf-8')
33
- return JSON.parse(data)
34
- } catch (err) {
35
- if (err.code === 'ENOENT') {
36
- // File doesn't exist yet — normal on first run
37
- return []
38
- }
39
- console.error(`[WorkflowStore] Failed to load workflows: ${err.message}`)
40
- return []
41
- }
14
+ function load() {
15
+ const db = getDb()
16
+ const rows = db.prepare('SELECT data FROM workflows ORDER BY name').all()
17
+ return rows.map(r => JSON.parse(r.data))
42
18
  }
43
19
 
44
20
  /**
45
- * Save workflows to local file
21
+ * Save workflows (replace all)
46
22
  * @param {Array} workflows - Array of workflow objects
47
23
  */
48
- async function save(workflows) {
49
- try {
50
- await fs.writeFile(WORKFLOW_FILE, JSON.stringify(workflows, null, 2), 'utf-8')
51
- console.log(`[WorkflowStore] Saved ${workflows.length} workflows to ${WORKFLOW_FILE}`)
52
- } catch (err) {
53
- console.error(`[WorkflowStore] Failed to save workflows: ${err.message}`)
54
- throw err
55
- }
24
+ function save(workflows) {
25
+ const db = getDb()
26
+ const tx = db.transaction(() => {
27
+ db.prepare('DELETE FROM workflows').run()
28
+ const insert = db.prepare('INSERT INTO workflows (id, name, data) VALUES (?, ?, ?)')
29
+ for (const w of workflows) {
30
+ insert.run(w.id, w.name, JSON.stringify(w))
31
+ }
32
+ })
33
+ tx()
34
+ console.log(`[WorkflowStore] Saved ${workflows.length} workflows`)
56
35
  }
57
36
 
58
37
  /**
59
38
  * Update last_run for a specific workflow
60
39
  * @param {string} workflowId - Workflow UUID
61
40
  */
62
- async function updateLastRun(workflowId) {
63
- const workflows = await load()
64
- const workflow = workflows.find(w => w.id === workflowId)
65
- if (workflow) {
66
- workflow.last_run = new Date().toISOString()
67
- await save(workflows)
68
- }
41
+ function updateLastRun(workflowId) {
42
+ const db = getDb()
43
+ const row = db.prepare('SELECT data FROM workflows WHERE id = ?').get(workflowId)
44
+ if (!row) return
45
+
46
+ const workflow = JSON.parse(row.data)
47
+ workflow.last_run = new Date().toISOString()
48
+ db.prepare('UPDATE workflows SET data = ? WHERE id = ?').run(JSON.stringify(workflow), workflowId)
69
49
  }
70
50
 
71
51
  /**
72
52
  * Find a workflow by name
73
53
  * @param {string} name - Workflow name (slug)
74
- * @returns {Promise<object|null>} Workflow object or null
54
+ * @returns {object|null} Workflow object or null
75
55
  */
76
- async function findByName(name) {
77
- const workflows = await load()
78
- return workflows.find(w => w.name === name) || null
56
+ function findByName(name) {
57
+ const db = getDb()
58
+ const row = db.prepare('SELECT data FROM workflows WHERE name = ?').get(name)
59
+ return row ? JSON.parse(row.data) : null
79
60
  }
80
61
 
81
62
  /**
@@ -83,27 +64,27 @@ async function findByName(name) {
83
64
  * If exists: updates definition only (preserves schedule/local state).
84
65
  * If new: creates with inactive schedule.
85
66
  * @param {object} workflowData - { name, pipeline_skill_names, pipeline_steps, content, project_id }
86
- * @returns {Promise<Array>} Updated workflows array
67
+ * @returns {Array} Updated workflows array
87
68
  */
88
- async function upsertByName(workflowData) {
89
- const crypto = require('crypto')
90
- const workflows = await load()
91
- const index = workflows.findIndex(w => w.name === workflowData.name)
69
+ function upsertByName(workflowData) {
70
+ const db = getDb()
71
+ const existing = db.prepare('SELECT data FROM workflows WHERE name = ?').get(workflowData.name)
92
72
 
93
- if (index >= 0) {
94
- // Update definition only (preserve schedule/local state)
95
- workflows[index].pipeline_skill_names = workflowData.pipeline_skill_names
73
+ if (existing) {
74
+ const workflow = JSON.parse(existing.data)
75
+ workflow.pipeline_skill_names = workflowData.pipeline_skill_names
96
76
  if (workflowData.content !== undefined) {
97
- workflows[index].content = workflowData.content
77
+ workflow.content = workflowData.content
98
78
  }
99
79
  if (workflowData.project_id !== undefined) {
100
- workflows[index].project_id = workflowData.project_id
80
+ workflow.project_id = workflowData.project_id
101
81
  }
102
82
  if (workflowData.pipeline_steps !== undefined) {
103
- workflows[index].pipeline_steps = workflowData.pipeline_steps
83
+ workflow.pipeline_steps = workflowData.pipeline_steps
104
84
  }
85
+ db.prepare('UPDATE workflows SET data = ? WHERE id = ?').run(JSON.stringify(workflow), workflow.id)
105
86
  } else {
106
- workflows.push({
87
+ const workflow = {
107
88
  id: crypto.randomUUID(),
108
89
  name: workflowData.name,
109
90
  pipeline_skill_names: workflowData.pipeline_skill_names,
@@ -113,11 +94,13 @@ async function upsertByName(workflowData) {
113
94
  cron_expression: '',
114
95
  is_active: false,
115
96
  last_run: null,
116
- })
97
+ }
98
+ db.prepare('INSERT INTO workflows (id, name, data) VALUES (?, ?, ?)').run(
99
+ workflow.id, workflow.name, JSON.stringify(workflow)
100
+ )
117
101
  }
118
102
 
119
- await save(workflows)
120
- return workflows
103
+ return load()
121
104
  }
122
105
 
123
106
  module.exports = { load, save, updateLastRun, findByName, upsertByName }
@@ -77,12 +77,14 @@ Files are stored in `~/files/`. Max upload size: 50MB.
77
77
 
78
78
  ### Memory (Long-term Knowledge)
79
79
 
80
- Persistent memory entries stored as markdown files in `$DATA_DIR/memory/`.
80
+ Persistent memory entries stored in SQLite (`$DATA_DIR/minion.db`).
81
81
  Categories: `user` (user preferences), `feedback` (feedback), `project` (project info), `reference` (references).
82
+ Full-text search supported via FTS5.
82
83
 
83
84
  | Method | Endpoint | Description |
84
85
  |--------|----------|-------------|
85
86
  | GET | `/api/memory` | List all memory entries (id, title, category, excerpt, timestamps) |
87
+ | GET | `/api/memory?search=keyword` | Full-text search on title and content (FTS5) |
86
88
  | GET | `/api/memory/:id` | Get full memory entry |
87
89
  | POST | `/api/memory` | Create memory entry. Body: `{title, category, content}` |
88
90
  | PUT | `/api/memory/:id` | Update memory entry. Body: `{title?, category?, content?}` |
@@ -116,12 +118,14 @@ Response (list):
116
118
 
117
119
  ### Daily Logs (Short-term Memory)
118
120
 
119
- Daily conversation summaries stored as `$DATA_DIR/daily-logs/YYYY-MM-DD.md`.
121
+ Daily conversation summaries stored in SQLite (`$DATA_DIR/minion.db`).
120
122
  Generated via end-of-day processing or manual creation.
123
+ Full-text search supported via FTS5.
121
124
 
122
125
  | Method | Endpoint | Description |
123
126
  |--------|----------|-------------|
124
127
  | GET | `/api/daily-logs` | List all logs (date + size, newest first) |
128
+ | GET | `/api/daily-logs?search=keyword` | Full-text search on log content (FTS5) |
125
129
  | POST | `/api/daily-logs` | Create a daily log. Body: `{date, content}` |
126
130
  | GET | `/api/daily-logs/:date` | Get a specific day's log content |
127
131
  | PUT | `/api/daily-logs/:date` | Update a daily log. Body: `{content}` |