@geekbeer/minion 3.34.0 → 3.40.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/db/helpers.js +18 -0
- package/core/db/index.js +146 -0
- package/core/db/migrations/000_initial_schema.js +157 -0
- package/core/db/migrations/001_fts_trigram.js +78 -0
- package/core/db/migrations/002_emails_fts.js +41 -0
- package/core/db/migrations/003_memories_project_id.js +17 -0
- package/core/db/migrations/004_chat_sessions_workspace.js +18 -0
- package/core/db/migrations/005_todos_session_injection.js +19 -0
- package/core/db/migrations/006_daily_logs_workspace.js +69 -0
- package/core/db/migrations/007_workspace_scoping.js +29 -0
- package/core/db/migrations/008_todos_workspace.js +22 -0
- package/core/db/migrations/index.js +41 -0
- package/core/lib/config-warnings.js +16 -8
- package/core/lib/dag-step-poller.js +59 -16
- package/core/lib/end-of-day.js +30 -14
- package/core/lib/reflection-scheduler.js +23 -9
- package/core/lib/thread-watcher.js +3 -0
- package/core/routes/daily-logs.js +64 -27
- package/core/routes/routines.js +6 -2
- package/core/routes/skills.js +4 -0
- package/core/routes/todos.js +20 -7
- package/core/routes/workflows.js +17 -7
- package/core/stores/daily-log-store.js +61 -30
- package/core/stores/execution-store.js +40 -18
- package/core/stores/routine-store.js +32 -14
- package/core/stores/todo-store.js +37 -10
- package/core/stores/workflow-store.js +34 -13
- package/docs/api-reference.md +80 -36
- package/docs/task-guides.md +51 -4
- package/linux/routes/chat.js +16 -10
- package/linux/routes/directives.js +4 -0
- package/linux/routine-runner.js +24 -5
- package/linux/workflow-runner.js +38 -12
- package/package.json +4 -2
- package/rules/core.md +4 -0
- package/scripts/new-migration.js +53 -0
- package/win/routes/chat.js +16 -10
- package/win/routes/directives.js +4 -0
- package/win/routine-runner.js +2 -0
- package/win/workflow-runner.js +5 -3
- package/core/db.js +0 -583
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Warning categories:
|
|
9
9
|
* - llm_not_authenticated: No LLM service is authenticated (Claude/Gemini/Codex)
|
|
10
|
-
* -
|
|
10
|
+
* - llm_plugin_primary_not_set: LLM plugin `primary` is not selected (required for workflow/routine execution)
|
|
11
|
+
* - llm_plugin_primary_not_enabled: `primary` plugin is not present in `enabled` list (inconsistent config)
|
|
11
12
|
* - tunnel_not_running: Cloudflare tunnel is not running (required for VNC/terminal access)
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
const { execSync } = require('child_process')
|
|
15
16
|
const { config } = require('../config')
|
|
16
|
-
const { getLlmServices
|
|
17
|
+
const { getLlmServices } = require('./llm-checker')
|
|
18
|
+
const { readConfig: readLlmPluginConfig } = require('../llm-plugins/registry')
|
|
17
19
|
const { IS_WINDOWS } = require('./platform')
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -59,15 +61,21 @@ function getConfigWarnings() {
|
|
|
59
61
|
if (!hasAuthenticatedLlm) {
|
|
60
62
|
warnings.push({
|
|
61
63
|
code: 'llm_not_authenticated',
|
|
62
|
-
message: 'LLM
|
|
64
|
+
message: 'LLMが未認証です。Claude/Gemini/Codexのいずれかを認証してください',
|
|
63
65
|
})
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
// Check
|
|
67
|
-
|
|
68
|
+
// Check LLM plugin selection
|
|
69
|
+
const pluginCfg = readLlmPluginConfig()
|
|
70
|
+
if (!pluginCfg.primary) {
|
|
68
71
|
warnings.push({
|
|
69
|
-
code: '
|
|
70
|
-
message: '
|
|
72
|
+
code: 'llm_plugin_primary_not_set',
|
|
73
|
+
message: 'LLMプラグインのprimaryが未設定です。ワークフロー/ルーティン実行に必要です',
|
|
74
|
+
})
|
|
75
|
+
} else if (!pluginCfg.enabled.includes(pluginCfg.primary)) {
|
|
76
|
+
warnings.push({
|
|
77
|
+
code: 'llm_plugin_primary_not_enabled',
|
|
78
|
+
message: `primaryプラグイン "${pluginCfg.primary}" がenabledに含まれていません`,
|
|
71
79
|
})
|
|
72
80
|
}
|
|
73
81
|
|
|
@@ -75,7 +83,7 @@ function getConfigWarnings() {
|
|
|
75
83
|
if (config.HQ_URL && !isTunnelRunning()) {
|
|
76
84
|
warnings.push({
|
|
77
85
|
code: 'tunnel_not_running',
|
|
78
|
-
message: 'Cloudflare
|
|
86
|
+
message: 'Cloudflareトンネルが停止しています。VNC/ターミナル接続ができません',
|
|
79
87
|
})
|
|
80
88
|
}
|
|
81
89
|
|
|
@@ -255,6 +255,7 @@ async function executeTransformNode(node) {
|
|
|
255
255
|
assigned_role,
|
|
256
256
|
input_data,
|
|
257
257
|
transform_instruction,
|
|
258
|
+
input_contracts,
|
|
258
259
|
output_contracts,
|
|
259
260
|
} = node
|
|
260
261
|
|
|
@@ -290,7 +291,12 @@ async function executeTransformNode(node) {
|
|
|
290
291
|
|
|
291
292
|
// 2. Create ephemeral skill from transform_instruction.
|
|
292
293
|
// Write to every active plugin's skill dir so any Primary can find it.
|
|
293
|
-
const skillContent = buildTransformSkillContent(
|
|
294
|
+
const skillContent = buildTransformSkillContent(
|
|
295
|
+
transform_instruction,
|
|
296
|
+
input_data,
|
|
297
|
+
input_contracts,
|
|
298
|
+
output_contracts,
|
|
299
|
+
)
|
|
294
300
|
for (const dir of ephemeralSkillDirs) {
|
|
295
301
|
await fs.mkdir(dir, { recursive: true })
|
|
296
302
|
await fs.writeFile(path.join(dir, 'SKILL.md'), skillContent, 'utf-8')
|
|
@@ -357,51 +363,88 @@ async function executeTransformNode(node) {
|
|
|
357
363
|
|
|
358
364
|
/**
|
|
359
365
|
* Build SKILL.md content for a transform node's ephemeral skill.
|
|
366
|
+
*
|
|
367
|
+
* Transform is contract-driven: the Output Contract is the authoritative
|
|
368
|
+
* target shape, enforced by HQ's runtime validator on node-complete.
|
|
369
|
+
* `instruction` is an optional hint layered on top for cases where the
|
|
370
|
+
* contract alone doesn't convey the intent (unit conversion, filter
|
|
371
|
+
* criteria, renaming conventions, etc.).
|
|
360
372
|
*/
|
|
361
|
-
function buildTransformSkillContent(instruction, inputData, outputContracts) {
|
|
373
|
+
function buildTransformSkillContent(instruction, inputData, inputContracts, outputContracts) {
|
|
362
374
|
const lines = [
|
|
363
375
|
'---',
|
|
364
376
|
'name: dag-transform',
|
|
365
377
|
'description: DAG Transform Node',
|
|
366
378
|
'---',
|
|
367
379
|
'',
|
|
368
|
-
'You are a data transformation step in a DAG workflow.',
|
|
380
|
+
'You are a data transformation step in a DAG workflow. Your job is to',
|
|
381
|
+
'reshape the Input Data into an object that conforms **exactly** to the',
|
|
382
|
+
'Output Contract. HQ will reject the result if any required field is',
|
|
383
|
+
'missing or has the wrong type.',
|
|
369
384
|
'',
|
|
370
385
|
'## Input Data',
|
|
371
386
|
'```json',
|
|
372
387
|
JSON.stringify(inputData, null, 2),
|
|
373
388
|
'```',
|
|
374
|
-
'',
|
|
375
|
-
'## Transform Instruction',
|
|
376
|
-
instruction,
|
|
377
389
|
]
|
|
378
390
|
|
|
391
|
+
if (inputContracts && inputContracts.length > 0) {
|
|
392
|
+
lines.push('', '## Input Contract')
|
|
393
|
+
lines.push('The Input Data above conforms to:')
|
|
394
|
+
for (const ic of inputContracts) {
|
|
395
|
+
lines.push(`### ${ic.contract_name}`)
|
|
396
|
+
lines.push(ic.contract.description || '')
|
|
397
|
+
appendContractTable(lines, ic.contract.fields || [])
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
379
401
|
if (outputContracts && outputContracts.length > 0) {
|
|
380
|
-
lines.push('', '## Output Contract')
|
|
381
|
-
lines.push(
|
|
402
|
+
lines.push('', '## Output Contract (REQUIRED)')
|
|
403
|
+
lines.push(
|
|
404
|
+
'Produce a JSON object matching this contract. Every required field',
|
|
405
|
+
'must be present with the declared type. Extra fields are discarded.',
|
|
406
|
+
)
|
|
382
407
|
for (const oc of outputContracts) {
|
|
383
408
|
lines.push(`### ${oc.contract_name}`)
|
|
384
409
|
lines.push(oc.contract.description || '')
|
|
385
|
-
lines.
|
|
386
|
-
lines.push('|-------|------|----------|-------------|')
|
|
387
|
-
for (const f of oc.contract.fields || []) {
|
|
388
|
-
const typeDisplay = f.type === 'array' && f.items ? `array<${f.items}>` : f.type
|
|
389
|
-
lines.push(`| ${f.key} | ${typeDisplay} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |`)
|
|
390
|
-
}
|
|
410
|
+
appendContractTable(lines, oc.contract.fields || [])
|
|
391
411
|
}
|
|
412
|
+
} else {
|
|
413
|
+
lines.push(
|
|
414
|
+
'',
|
|
415
|
+
'## Output Contract',
|
|
416
|
+
'⚠️ No output contract was declared on the outgoing edge. This transform',
|
|
417
|
+
'node is misconfigured and should not have reached execution. Return',
|
|
418
|
+
'the input data unchanged and fail loudly.',
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (instruction && instruction.trim()) {
|
|
423
|
+
lines.push('', '## Hint (optional)')
|
|
424
|
+
lines.push(instruction.trim())
|
|
392
425
|
}
|
|
393
426
|
|
|
394
427
|
lines.push(
|
|
395
428
|
'',
|
|
396
429
|
'## Task',
|
|
397
|
-
'
|
|
398
|
-
'
|
|
430
|
+
'1. Read Input Data and the Output Contract carefully.',
|
|
431
|
+
'2. Construct a JSON object satisfying the Output Contract.',
|
|
432
|
+
'3. Output it under a "## Output Data" section with a json code block.',
|
|
399
433
|
'Do NOT output anything other than the Output Data section.',
|
|
400
434
|
)
|
|
401
435
|
|
|
402
436
|
return lines.join('\n')
|
|
403
437
|
}
|
|
404
438
|
|
|
439
|
+
function appendContractTable(lines, fields) {
|
|
440
|
+
lines.push('| Field | Type | Required | Description |')
|
|
441
|
+
lines.push('|-------|------|----------|-------------|')
|
|
442
|
+
for (const f of fields) {
|
|
443
|
+
const typeDisplay = f.type === 'array' && f.items ? `array<${f.items}>` : f.type
|
|
444
|
+
lines.push(`| ${f.name} | ${typeDisplay} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |`)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
405
448
|
/**
|
|
406
449
|
* Resolve skill_version_id to skill name via HQ API.
|
|
407
450
|
*/
|
package/core/lib/end-of-day.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared end-of-day processing logic
|
|
3
3
|
*
|
|
4
|
-
* Extracts daily conversation summary and long-term learnings from the chat session
|
|
5
|
-
* Used by both the chat route (manual trigger) and the
|
|
4
|
+
* Extracts daily conversation summary and long-term learnings from the chat session
|
|
5
|
+
* of a specific workspace. Used by both the chat route (manual trigger) and the
|
|
6
|
+
* reflection scheduler (automatic, loops over all workspaces).
|
|
6
7
|
*
|
|
7
8
|
* @module core/lib/end-of-day
|
|
8
9
|
*/
|
|
@@ -10,19 +11,35 @@
|
|
|
10
11
|
const chatStore = require('../stores/chat-store')
|
|
11
12
|
const memoryStore = require('../stores/memory-store')
|
|
12
13
|
const dailyLogStore = require('../stores/daily-log-store')
|
|
14
|
+
const workspaceStore = require('../stores/workspace-store')
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
|
-
* Run end-of-day processing: generate daily log + extract
|
|
17
|
+
* Run end-of-day processing for a single workspace: generate daily log + extract
|
|
18
|
+
* memories from the workspace's chat session. If no conversation exists, a stub
|
|
19
|
+
* log is saved so that the absence of activity is itself a record.
|
|
16
20
|
*
|
|
17
21
|
* @param {Object} opts
|
|
22
|
+
* @param {string} opts.workspaceId - Workspace to process ('' = unassigned)
|
|
18
23
|
* @param {(prompt: string) => Promise<string>} opts.runQuickLlmCall - Platform-specific LLM call function
|
|
19
24
|
* @param {boolean} [opts.clearSession=false] - Whether to clear the chat session after processing
|
|
20
|
-
* @returns {Promise<{ daily_log: string|null, memory_entries_added: number }>}
|
|
25
|
+
* @returns {Promise<{ daily_log: string|null, memory_entries_added: number, had_conversation: boolean }>}
|
|
21
26
|
*/
|
|
22
|
-
async function runEndOfDay({ runQuickLlmCall, clearSession = false }) {
|
|
23
|
-
|
|
27
|
+
async function runEndOfDay({ workspaceId, runQuickLlmCall, clearSession = false }) {
|
|
28
|
+
if (workspaceId == null) {
|
|
29
|
+
throw new Error('workspaceId is required for end-of-day processing')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const today = new Date().toISOString().split('T')[0]
|
|
33
|
+
const session = await chatStore.load(workspaceId)
|
|
34
|
+
|
|
35
|
+
// No conversation — record a stub so the idle day is still logged.
|
|
24
36
|
if (!session || session.messages.length === 0) {
|
|
25
|
-
|
|
37
|
+
const ws = workspaceId ? workspaceStore.getById(workspaceId) : null
|
|
38
|
+
const wsLabel = ws ? `${ws.name}` : (workspaceId ? `workspace:${workspaceId}` : '未所属')
|
|
39
|
+
const stub = `# ${today} (${wsLabel})\n\n本日、このワークスペースでの会話はありませんでした。`
|
|
40
|
+
await dailyLogStore.saveLog(workspaceId, today, stub)
|
|
41
|
+
console.log(`[EndOfDay] No conversation in workspace=${workspaceId || '(unassigned)'}, saved stub log for ${today}`)
|
|
42
|
+
return { daily_log: today, memory_entries_added: 0, had_conversation: false }
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
// Build conversation text for summarization (last 50 messages)
|
|
@@ -52,7 +69,6 @@ ${conversationText}
|
|
|
52
69
|
|
|
53
70
|
JSON形式のみで回答してください(\`\`\`jsonブロックは不要):`
|
|
54
71
|
|
|
55
|
-
const today = new Date().toISOString().split('T')[0]
|
|
56
72
|
let dailyLog = null
|
|
57
73
|
let memoriesAdded = 0
|
|
58
74
|
|
|
@@ -73,8 +89,8 @@ JSON形式のみで回答してください(\`\`\`jsonブロックは不要)
|
|
|
73
89
|
// Save daily log
|
|
74
90
|
if (parsed.daily_summary) {
|
|
75
91
|
dailyLog = parsed.daily_summary
|
|
76
|
-
await dailyLogStore.saveLog(today, dailyLog)
|
|
77
|
-
console.log(`[EndOfDay] Saved daily log for ${today}`)
|
|
92
|
+
await dailyLogStore.saveLog(workspaceId, today, dailyLog)
|
|
93
|
+
console.log(`[EndOfDay] Saved daily log for ${today} workspace=${workspaceId || '(unassigned)'}`)
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
// Save learnings to memory
|
|
@@ -102,15 +118,15 @@ JSON形式のみで回答してください(\`\`\`jsonブロックは不要)
|
|
|
102
118
|
.map(m => `**${m.role === 'user' ? 'ユーザー' : 'アシスタント'}**: ${m.content.substring(0, 300)}`)
|
|
103
119
|
.join('\n\n')
|
|
104
120
|
dailyLog = `# ${today} 会話ログ\n\n${fallback}`
|
|
105
|
-
await dailyLogStore.saveLog(today, dailyLog)
|
|
121
|
+
await dailyLogStore.saveLog(workspaceId, today, dailyLog)
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
if (clearSession) {
|
|
109
|
-
await chatStore.clear()
|
|
110
|
-
console.log(
|
|
125
|
+
await chatStore.clear(workspaceId)
|
|
126
|
+
console.log(`[EndOfDay] Session cleared for workspace=${workspaceId || '(unassigned)'}`)
|
|
111
127
|
}
|
|
112
128
|
|
|
113
|
-
return { daily_log: today, memory_entries_added: memoriesAdded }
|
|
129
|
+
return { daily_log: today, memory_entries_added: memoriesAdded, had_conversation: true }
|
|
114
130
|
}
|
|
115
131
|
|
|
116
132
|
module.exports = { runEndOfDay }
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
const { Cron } = require('croner')
|
|
20
20
|
const { config } = require('../config')
|
|
21
21
|
const { runEndOfDay } = require('./end-of-day')
|
|
22
|
+
const workspaceStore = require('../stores/workspace-store')
|
|
22
23
|
|
|
23
24
|
/** @type {import('croner').Cron | null} */
|
|
24
25
|
let cronJob = null
|
|
@@ -134,16 +135,29 @@ async function runReflection() {
|
|
|
134
135
|
console.log('[ReflectionScheduler] Starting daily reflection...')
|
|
135
136
|
|
|
136
137
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
138
|
+
// Always include '' (unassigned) so pre-workspace-scoping legacy conversations
|
|
139
|
+
// (or minions not yet synced with HQ) are still processed.
|
|
140
|
+
const workspaces = workspaceStore.list()
|
|
141
|
+
const workspaceIds = ['', ...workspaces.map(w => w.id)]
|
|
142
|
+
|
|
143
|
+
let totalMemories = 0
|
|
144
|
+
let totalLogs = 0
|
|
145
|
+
|
|
146
|
+
for (const workspaceId of workspaceIds) {
|
|
147
|
+
try {
|
|
148
|
+
const result = await runEndOfDay({
|
|
149
|
+
workspaceId,
|
|
150
|
+
runQuickLlmCall: llmCallFn,
|
|
151
|
+
clearSession: false,
|
|
152
|
+
})
|
|
153
|
+
if (result.daily_log) totalLogs++
|
|
154
|
+
totalMemories += result.memory_entries_added
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error(`[ReflectionScheduler] Workspace ${workspaceId || '(unassigned)'} failed: ${err.message}`)
|
|
157
|
+
}
|
|
146
158
|
}
|
|
159
|
+
|
|
160
|
+
console.log(`[ReflectionScheduler] Completed: ${totalLogs} logs across ${workspaceIds.length} workspaces, ${totalMemories} memories`)
|
|
147
161
|
} catch (err) {
|
|
148
162
|
console.error(`[ReflectionScheduler] Failed: ${err.message}`)
|
|
149
163
|
} finally {
|
|
@@ -310,6 +310,8 @@ ${roleGuidance}
|
|
|
310
310
|
const linkedProjectId = (threadSummary.linked_projects && threadSummary.linked_projects.length > 0)
|
|
311
311
|
? threadSummary.linked_projects[0].id
|
|
312
312
|
: null
|
|
313
|
+
// HQ resolves the thread's workspace_id on delivery (see
|
|
314
|
+
// src/app/api/minion/threads/route.ts). Fall back to '' if missing.
|
|
313
315
|
todoStore.add({
|
|
314
316
|
title: parsed.todo.title,
|
|
315
317
|
priority: parsed.todo.priority || 'normal',
|
|
@@ -317,6 +319,7 @@ ${roleGuidance}
|
|
|
317
319
|
source_type: 'thread',
|
|
318
320
|
source_id: threadSummary.id,
|
|
319
321
|
project_id: linkedProjectId,
|
|
322
|
+
workspace_id: threadSummary.workspace_id || '',
|
|
320
323
|
})
|
|
321
324
|
console.log(`[ThreadWatcher] Created TODO from thread: "${parsed.todo.title}"`)
|
|
322
325
|
} catch (err) {
|
|
@@ -1,40 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Daily log endpoints
|
|
3
3
|
*
|
|
4
|
-
* Provides CRUD + search for daily conversation summaries.
|
|
4
|
+
* Provides CRUD + search for daily conversation summaries, scoped by workspace.
|
|
5
5
|
* Logs are stored in SQLite with FTS5 full-text search.
|
|
6
6
|
*
|
|
7
|
+
* All endpoints require a `workspace_id` identifying the scope. Pass `''`
|
|
8
|
+
* (empty string) to address legacy / unassigned logs.
|
|
9
|
+
*
|
|
7
10
|
* Endpoints:
|
|
8
|
-
* GET /api/daily-logs
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* GET /api/daily-logs?workspace_id=... - List logs
|
|
12
|
+
* GET /api/daily-logs?workspace_id=...&search=keyword - FTS5 search
|
|
13
|
+
* POST /api/daily-logs - Create (body: workspace_id, date, content)
|
|
14
|
+
* GET /api/daily-logs/:date?workspace_id=... - Get a specific day's log
|
|
15
|
+
* PUT /api/daily-logs/:date - Update (body: workspace_id, content)
|
|
16
|
+
* DELETE /api/daily-logs/:date?workspace_id=... - Delete
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
19
|
const { verifyToken } = require('../lib/auth')
|
|
16
20
|
const dailyLogStore = require('../stores/daily-log-store')
|
|
17
21
|
|
|
22
|
+
function pickWorkspaceId(raw) {
|
|
23
|
+
// Accept '' (explicit unassigned) but reject undefined/null.
|
|
24
|
+
if (raw === '' || typeof raw === 'string') return raw
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
/**
|
|
19
29
|
* Register daily log routes as Fastify plugin
|
|
20
30
|
* @param {import('fastify').FastifyInstance} fastify
|
|
21
31
|
*/
|
|
22
32
|
async function dailyLogRoutes(fastify) {
|
|
23
33
|
|
|
24
|
-
// GET /api/daily-logs - List or search logs
|
|
34
|
+
// GET /api/daily-logs - List or search logs (scoped)
|
|
25
35
|
fastify.get('/api/daily-logs', async (request, reply) => {
|
|
26
36
|
if (!verifyToken(request)) {
|
|
27
37
|
reply.code(401)
|
|
28
38
|
return { success: false, error: 'Unauthorized' }
|
|
29
39
|
}
|
|
30
40
|
|
|
41
|
+
const workspaceId = pickWorkspaceId(request.query?.workspace_id)
|
|
42
|
+
if (workspaceId === null) {
|
|
43
|
+
reply.code(400)
|
|
44
|
+
return { success: false, error: 'workspace_id query param is required' }
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
const { search } = request.query || {}
|
|
32
48
|
if (search) {
|
|
33
|
-
const logs = dailyLogStore.searchLogs(search)
|
|
49
|
+
const logs = dailyLogStore.searchLogs(workspaceId, search)
|
|
34
50
|
return { success: true, logs }
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
const logs = dailyLogStore.listLogs()
|
|
53
|
+
const logs = dailyLogStore.listLogs(workspaceId)
|
|
38
54
|
return { success: true, logs }
|
|
39
55
|
})
|
|
40
56
|
|
|
@@ -45,39 +61,50 @@ async function dailyLogRoutes(fastify) {
|
|
|
45
61
|
return { success: false, error: 'Unauthorized' }
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
const { date, content } = request.body || {}
|
|
64
|
+
const { workspace_id, date, content } = request.body || {}
|
|
65
|
+
const workspaceId = pickWorkspaceId(workspace_id)
|
|
66
|
+
if (workspaceId === null) {
|
|
67
|
+
reply.code(400)
|
|
68
|
+
return { success: false, error: 'workspace_id is required' }
|
|
69
|
+
}
|
|
49
70
|
if (!date || !content) {
|
|
50
71
|
reply.code(400)
|
|
51
72
|
return { success: false, error: 'date and content are required' }
|
|
52
73
|
}
|
|
53
74
|
|
|
54
|
-
// Check if log already exists
|
|
55
|
-
const existing = dailyLogStore.loadLog(date)
|
|
75
|
+
// Check if log already exists for this (workspace, date)
|
|
76
|
+
const existing = dailyLogStore.loadLog(workspaceId, date)
|
|
56
77
|
if (existing !== null) {
|
|
57
78
|
reply.code(409)
|
|
58
79
|
return { success: false, error: 'Log already exists for this date. Use PUT to update.' }
|
|
59
80
|
}
|
|
60
81
|
|
|
61
|
-
dailyLogStore.saveLog(date, content)
|
|
62
|
-
console.log(`[DailyLogs] Created log: ${date}`)
|
|
63
|
-
return { success: true, log: { date, content } }
|
|
82
|
+
dailyLogStore.saveLog(workspaceId, date, content)
|
|
83
|
+
console.log(`[DailyLogs] Created log: ${date} workspace=${workspaceId || '(unassigned)'}`)
|
|
84
|
+
return { success: true, log: { workspace_id: workspaceId, date, content } }
|
|
64
85
|
})
|
|
65
86
|
|
|
66
|
-
// GET /api/daily-logs/:date - Get a specific day's log
|
|
87
|
+
// GET /api/daily-logs/:date - Get a specific day's log for a workspace
|
|
67
88
|
fastify.get('/api/daily-logs/:date', async (request, reply) => {
|
|
68
89
|
if (!verifyToken(request)) {
|
|
69
90
|
reply.code(401)
|
|
70
91
|
return { success: false, error: 'Unauthorized' }
|
|
71
92
|
}
|
|
72
93
|
|
|
94
|
+
const workspaceId = pickWorkspaceId(request.query?.workspace_id)
|
|
95
|
+
if (workspaceId === null) {
|
|
96
|
+
reply.code(400)
|
|
97
|
+
return { success: false, error: 'workspace_id query param is required' }
|
|
98
|
+
}
|
|
99
|
+
|
|
73
100
|
const { date } = request.params
|
|
74
|
-
const content = dailyLogStore.loadLog(date)
|
|
101
|
+
const content = dailyLogStore.loadLog(workspaceId, date)
|
|
75
102
|
if (content === null) {
|
|
76
103
|
reply.code(404)
|
|
77
104
|
return { success: false, error: 'Log not found' }
|
|
78
105
|
}
|
|
79
106
|
|
|
80
|
-
return { success: true, log: { date, content } }
|
|
107
|
+
return { success: true, log: { workspace_id: workspaceId, date, content } }
|
|
81
108
|
})
|
|
82
109
|
|
|
83
110
|
// PUT /api/daily-logs/:date - Update a daily log
|
|
@@ -87,23 +114,27 @@ async function dailyLogRoutes(fastify) {
|
|
|
87
114
|
return { success: false, error: 'Unauthorized' }
|
|
88
115
|
}
|
|
89
116
|
|
|
90
|
-
const {
|
|
91
|
-
const
|
|
117
|
+
const { workspace_id, content } = request.body || {}
|
|
118
|
+
const workspaceId = pickWorkspaceId(workspace_id)
|
|
119
|
+
if (workspaceId === null) {
|
|
120
|
+
reply.code(400)
|
|
121
|
+
return { success: false, error: 'workspace_id is required' }
|
|
122
|
+
}
|
|
92
123
|
if (!content) {
|
|
93
124
|
reply.code(400)
|
|
94
125
|
return { success: false, error: 'content is required' }
|
|
95
126
|
}
|
|
96
127
|
|
|
97
|
-
|
|
98
|
-
const existing = dailyLogStore.loadLog(date)
|
|
128
|
+
const { date } = request.params
|
|
129
|
+
const existing = dailyLogStore.loadLog(workspaceId, date)
|
|
99
130
|
if (existing === null) {
|
|
100
131
|
reply.code(404)
|
|
101
132
|
return { success: false, error: 'Log not found' }
|
|
102
133
|
}
|
|
103
134
|
|
|
104
|
-
dailyLogStore.saveLog(date, content)
|
|
105
|
-
console.log(`[DailyLogs] Updated log: ${date}`)
|
|
106
|
-
return { success: true, log: { date, content } }
|
|
135
|
+
dailyLogStore.saveLog(workspaceId, date, content)
|
|
136
|
+
console.log(`[DailyLogs] Updated log: ${date} workspace=${workspaceId || '(unassigned)'}`)
|
|
137
|
+
return { success: true, log: { workspace_id: workspaceId, date, content } }
|
|
107
138
|
})
|
|
108
139
|
|
|
109
140
|
// DELETE /api/daily-logs/:date - Delete a specific day's log
|
|
@@ -113,14 +144,20 @@ async function dailyLogRoutes(fastify) {
|
|
|
113
144
|
return { success: false, error: 'Unauthorized' }
|
|
114
145
|
}
|
|
115
146
|
|
|
147
|
+
const workspaceId = pickWorkspaceId(request.query?.workspace_id)
|
|
148
|
+
if (workspaceId === null) {
|
|
149
|
+
reply.code(400)
|
|
150
|
+
return { success: false, error: 'workspace_id query param is required' }
|
|
151
|
+
}
|
|
152
|
+
|
|
116
153
|
const { date } = request.params
|
|
117
|
-
const deleted = dailyLogStore.deleteLog(date)
|
|
154
|
+
const deleted = dailyLogStore.deleteLog(workspaceId, date)
|
|
118
155
|
if (!deleted) {
|
|
119
156
|
reply.code(404)
|
|
120
157
|
return { success: false, error: 'Log not found' }
|
|
121
158
|
}
|
|
122
159
|
|
|
123
|
-
console.log(`[DailyLogs] Deleted log: ${date}`)
|
|
160
|
+
console.log(`[DailyLogs] Deleted log: ${date} workspace=${workspaceId || '(unassigned)'}`)
|
|
124
161
|
return { success: true }
|
|
125
162
|
})
|
|
126
163
|
}
|
package/core/routes/routines.js
CHANGED
|
@@ -69,14 +69,18 @@ async function routineRoutes(fastify, opts) {
|
|
|
69
69
|
}
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
// List
|
|
72
|
+
// List routines with their current status.
|
|
73
|
+
// Optional ?workspace_id= filter: when provided, scope to that workspace;
|
|
74
|
+
// when absent, return all.
|
|
73
75
|
fastify.get('/api/routines', async (request, reply) => {
|
|
74
76
|
if (!verifyToken(request)) {
|
|
75
77
|
reply.code(401)
|
|
76
78
|
return { success: false, error: 'Unauthorized' }
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
const
|
|
81
|
+
const rawWs = request.query?.workspace_id
|
|
82
|
+
const workspaceId = (rawWs === '' || typeof rawWs === 'string') ? rawWs : undefined
|
|
83
|
+
const routines = await routineStore.load(workspaceId)
|
|
80
84
|
const status = routineRunner.getStatus()
|
|
81
85
|
|
|
82
86
|
// Merge routine data with runtime status
|
package/core/routes/skills.js
CHANGED
|
@@ -340,6 +340,7 @@ async function skillRoutes(fastify, opts) {
|
|
|
340
340
|
execution_id,
|
|
341
341
|
step_index,
|
|
342
342
|
workflow_name,
|
|
343
|
+
workspace_id,
|
|
343
344
|
role,
|
|
344
345
|
revision_feedback,
|
|
345
346
|
review_history,
|
|
@@ -380,6 +381,7 @@ async function skillRoutes(fastify, opts) {
|
|
|
380
381
|
skill_name,
|
|
381
382
|
workflow_id: null,
|
|
382
383
|
workflow_name: workflow_name || null,
|
|
384
|
+
workspace_id: workspace_id || '',
|
|
383
385
|
status: 'running',
|
|
384
386
|
outcome: null,
|
|
385
387
|
started_at: startedAt,
|
|
@@ -407,6 +409,7 @@ async function skillRoutes(fastify, opts) {
|
|
|
407
409
|
const syntheticWorkflow = {
|
|
408
410
|
id: effectiveExecutionId,
|
|
409
411
|
name: workflow_name || skill_name,
|
|
412
|
+
workspace_id: workspace_id || '',
|
|
410
413
|
}
|
|
411
414
|
const result = await workflowRunner.runWorkflow({
|
|
412
415
|
...syntheticWorkflow,
|
|
@@ -421,6 +424,7 @@ async function skillRoutes(fastify, opts) {
|
|
|
421
424
|
skill_name,
|
|
422
425
|
workflow_id: null,
|
|
423
426
|
workflow_name: workflow_name || null,
|
|
427
|
+
workspace_id: workspace_id || '',
|
|
424
428
|
status: 'failed',
|
|
425
429
|
outcome: 'failure',
|
|
426
430
|
started_at: startedAt,
|
package/core/routes/todos.js
CHANGED
|
@@ -22,18 +22,22 @@ const todoStore = require('../stores/todo-store')
|
|
|
22
22
|
*/
|
|
23
23
|
async function todoRoutes(fastify) {
|
|
24
24
|
|
|
25
|
-
// GET /api/todos/summary - Status counts (must be before /:id)
|
|
25
|
+
// GET /api/todos/summary - Status counts (must be before /:id).
|
|
26
|
+
// Optional ?workspace_id= scopes the counts.
|
|
26
27
|
fastify.get('/api/todos/summary', async (request, reply) => {
|
|
27
28
|
if (!verifyToken(request)) {
|
|
28
29
|
reply.code(401)
|
|
29
30
|
return { success: false, error: 'Unauthorized' }
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
const
|
|
33
|
+
const rawWs = request.query?.workspace_id
|
|
34
|
+
const workspaceId = (rawWs === '' || typeof rawWs === 'string') ? rawWs : undefined
|
|
35
|
+
const summary = todoStore.getSummary(workspaceId)
|
|
33
36
|
return { success: true, summary }
|
|
34
37
|
})
|
|
35
38
|
|
|
36
|
-
// GET /api/todos - List with optional filters
|
|
39
|
+
// GET /api/todos - List with optional filters.
|
|
40
|
+
// Optional ?workspace_id= scopes to a workspace; omit for cross-workspace view.
|
|
37
41
|
fastify.get('/api/todos', async (request, reply) => {
|
|
38
42
|
if (!verifyToken(request)) {
|
|
39
43
|
reply.code(401)
|
|
@@ -41,12 +45,15 @@ async function todoRoutes(fastify) {
|
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
const { status, priority, project_id, source_type, session_id, limit } = request.query || {}
|
|
48
|
+
const rawWs = request.query?.workspace_id
|
|
49
|
+
const workspaceId = (rawWs === '' || typeof rawWs === 'string') ? rawWs : undefined
|
|
44
50
|
const todos = todoStore.list({
|
|
45
51
|
status,
|
|
46
52
|
priority,
|
|
47
53
|
project_id,
|
|
48
54
|
source_type,
|
|
49
55
|
session_id,
|
|
56
|
+
workspace_id: workspaceId,
|
|
50
57
|
limit: limit ? parseInt(limit, 10) : undefined,
|
|
51
58
|
})
|
|
52
59
|
|
|
@@ -69,22 +76,28 @@ async function todoRoutes(fastify) {
|
|
|
69
76
|
return { success: true, todo }
|
|
70
77
|
})
|
|
71
78
|
|
|
72
|
-
// POST /api/todos - Create a new todo
|
|
79
|
+
// POST /api/todos - Create a new todo.
|
|
80
|
+
// workspace_id is required (pass "" for unassigned/legacy); no auto-resolution
|
|
81
|
+
// is performed so callers must always decide the scope explicitly.
|
|
73
82
|
fastify.post('/api/todos', async (request, reply) => {
|
|
74
83
|
if (!verifyToken(request)) {
|
|
75
84
|
reply.code(401)
|
|
76
85
|
return { success: false, error: 'Unauthorized' }
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
const { title, description, priority, source_type, source_id, project_id, due_at, data, session_id } = request.body || {}
|
|
88
|
+
const { title, description, priority, source_type, source_id, project_id, workspace_id, due_at, data, session_id } = request.body || {}
|
|
80
89
|
if (!title) {
|
|
81
90
|
reply.code(400)
|
|
82
91
|
return { success: false, error: 'title is required' }
|
|
83
92
|
}
|
|
93
|
+
if (workspace_id !== '' && typeof workspace_id !== 'string') {
|
|
94
|
+
reply.code(400)
|
|
95
|
+
return { success: false, error: 'workspace_id is required (pass "" for unassigned/legacy todos)' }
|
|
96
|
+
}
|
|
84
97
|
|
|
85
98
|
try {
|
|
86
|
-
const todo = todoStore.add({ title, description, priority, source_type, source_id, project_id, due_at, data, session_id })
|
|
87
|
-
console.log(`[Todos] Created: ${todo.id} "${todo.title}"`)
|
|
99
|
+
const todo = todoStore.add({ title, description, priority, source_type, source_id, project_id, workspace_id, due_at, data, session_id })
|
|
100
|
+
console.log(`[Todos] Created: ${todo.id} "${todo.title}" ws=${workspace_id || '(unassigned)'}`)
|
|
88
101
|
reply.code(201)
|
|
89
102
|
return { success: true, todo }
|
|
90
103
|
} catch (err) {
|