optimal-cli 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +10 -0
- package/.env.example +17 -0
- package/CLAUDE.md +67 -0
- package/COMMANDS.md +264 -0
- package/PUBLISH.md +70 -0
- package/agents/content-ops.md +2 -2
- package/agents/financial-ops.md +2 -2
- package/agents/infra-ops.md +2 -2
- package/apps/.gitkeep +0 -0
- package/bin/optimal.ts +278 -591
- package/docs/MIGRATION_NEEDED.md +37 -0
- package/docs/plans/.gitkeep +0 -0
- package/docs/plans/optimal-cli-config-registry-v1.md +71 -0
- package/hooks/.gitkeep +0 -0
- package/lib/config/registry.ts +5 -4
- package/lib/kanban-obsidian.ts +232 -0
- package/lib/kanban-sync.ts +258 -0
- package/lib/kanban.ts +239 -0
- package/lib/obsidian-tasks.ts +231 -0
- package/package.json +5 -19
- package/pnpm-workspace.yaml +3 -0
- package/scripts/check-table.ts +24 -0
- package/scripts/create-tables.ts +94 -0
- package/scripts/migrate-kanban.sh +28 -0
- package/scripts/migrate-v2.ts +78 -0
- package/scripts/migrate.ts +79 -0
- package/scripts/run-migration.ts +59 -0
- package/scripts/seed-board.ts +203 -0
- package/scripts/test-kanban.ts +21 -0
- package/skills/audit-financials/SKILL.md +33 -0
- package/skills/board-create/SKILL.md +28 -0
- package/skills/board-update/SKILL.md +27 -0
- package/skills/board-view/SKILL.md +27 -0
- package/skills/delete-batch/SKILL.md +77 -0
- package/skills/deploy/SKILL.md +40 -0
- package/skills/diagnose-months/SKILL.md +68 -0
- package/skills/distribute-newsletter/SKILL.md +58 -0
- package/skills/export-budget/SKILL.md +44 -0
- package/skills/export-kpis/SKILL.md +52 -0
- package/skills/generate-netsuite-template/SKILL.md +51 -0
- package/skills/generate-newsletter/SKILL.md +53 -0
- package/skills/generate-newsletter-insurance/SKILL.md +59 -0
- package/skills/generate-social-posts/SKILL.md +67 -0
- package/skills/health-check/SKILL.md +42 -0
- package/skills/ingest-transactions/SKILL.md +51 -0
- package/skills/manage-cms/SKILL.md +50 -0
- package/skills/manage-scenarios/SKILL.md +83 -0
- package/skills/migrate-db/SKILL.md +79 -0
- package/skills/preview-newsletter/SKILL.md +50 -0
- package/skills/project-budget/SKILL.md +60 -0
- package/skills/publish-blog/SKILL.md +70 -0
- package/skills/publish-social-posts/SKILL.md +70 -0
- package/skills/rate-anomalies/SKILL.md +62 -0
- package/skills/scrape-ads/SKILL.md +49 -0
- package/skills/stamp-transactions/SKILL.md +62 -0
- package/skills/upload-income-statements/SKILL.md +54 -0
- package/skills/upload-netsuite/SKILL.md +56 -0
- package/skills/upload-r1/SKILL.md +45 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/migrations/.gitkeep +0 -0
- package/supabase/migrations/20250305000001_create_agent_configs.sql +36 -0
- package/supabase/migrations/20260305111300_create_cli_config_registry.sql +22 -0
- package/supabase/migrations/20260306195000_create_kanban_tables.sql +97 -0
- package/tests/config-command-smoke.test.ts +395 -0
- package/tests/config-registry.test.ts +173 -0
- package/tsconfig.json +19 -0
- package/agents/profiles.json +0 -5
- package/docs/CLI-REFERENCE.md +0 -361
- package/lib/assets/index.ts +0 -225
- package/lib/assets.ts +0 -124
- package/lib/auth/index.ts +0 -189
- package/lib/board/index.ts +0 -309
- package/lib/board/types.ts +0 -124
- package/lib/bot/claim.ts +0 -43
- package/lib/bot/coordinator.ts +0 -254
- package/lib/bot/heartbeat.ts +0 -37
- package/lib/bot/index.ts +0 -9
- package/lib/bot/protocol.ts +0 -99
- package/lib/bot/reporter.ts +0 -42
- package/lib/bot/skills.ts +0 -81
- package/lib/errors.ts +0 -129
- package/lib/format.ts +0 -120
- package/lib/returnpro/validate.ts +0 -154
- package/lib/social/meta.ts +0 -228
package/lib/kanban.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase-backed Kanban operations
|
|
3
|
+
* Uses existing projects and tasks tables in supabase
|
|
4
|
+
*/
|
|
5
|
+
import { getSupabase } from './supabase.js'
|
|
6
|
+
|
|
7
|
+
const sb = () => getSupabase('optimal')
|
|
8
|
+
|
|
9
|
+
// --- Types ---
|
|
10
|
+
|
|
11
|
+
export interface Task {
|
|
12
|
+
id: string
|
|
13
|
+
project_id: string
|
|
14
|
+
milestone_id: string | null
|
|
15
|
+
title: string
|
|
16
|
+
description: string | null
|
|
17
|
+
status: 'backlog' | 'ready' | 'in_progress' | 'blocked' | 'review' | 'done' | 'cancelled'
|
|
18
|
+
priority: number
|
|
19
|
+
assigned_to: string | null
|
|
20
|
+
claimed_by: string | null
|
|
21
|
+
claimed_at: string | null
|
|
22
|
+
skill_required: string | null
|
|
23
|
+
source_repo: string | null
|
|
24
|
+
target_module: string | null
|
|
25
|
+
estimated_effort: string | null
|
|
26
|
+
blocked_by: string[]
|
|
27
|
+
sort_order: number
|
|
28
|
+
created_at: string
|
|
29
|
+
updated_at: string
|
|
30
|
+
completed_at: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Project {
|
|
34
|
+
id: string
|
|
35
|
+
slug: string
|
|
36
|
+
name: string
|
|
37
|
+
description: string | null
|
|
38
|
+
status: 'active' | 'archived' | 'on_hold'
|
|
39
|
+
priority: number
|
|
40
|
+
owner: string | null
|
|
41
|
+
created_at: string
|
|
42
|
+
updated_at: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CreateTaskInput {
|
|
46
|
+
project_slug: string
|
|
47
|
+
title: string
|
|
48
|
+
description?: string
|
|
49
|
+
priority?: number
|
|
50
|
+
skill_required?: string
|
|
51
|
+
source_repo?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- Projects ---
|
|
55
|
+
|
|
56
|
+
export async function getProjectBySlug(slug: string): Promise<Project> {
|
|
57
|
+
const { data, error } = await sb()
|
|
58
|
+
.from('projects')
|
|
59
|
+
.select('*')
|
|
60
|
+
.eq('slug', slug)
|
|
61
|
+
.single()
|
|
62
|
+
|
|
63
|
+
if (error) throw new Error(`Project not found: ${slug} — ${error.message}`)
|
|
64
|
+
return data as Project
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function listProjects(): Promise<Project[]> {
|
|
68
|
+
const { data, error } = await sb()
|
|
69
|
+
.from('projects')
|
|
70
|
+
.select('*')
|
|
71
|
+
.eq('status', 'active')
|
|
72
|
+
.order('name')
|
|
73
|
+
|
|
74
|
+
if (error) throw new Error(`Failed to list projects: ${error.message}`)
|
|
75
|
+
return (data ?? []) as Project[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- Tasks ---
|
|
79
|
+
|
|
80
|
+
export async function createTask(input: CreateTaskInput): Promise<Task> {
|
|
81
|
+
const project = await getProjectBySlug(input.project_slug)
|
|
82
|
+
|
|
83
|
+
const { data, error } = await sb()
|
|
84
|
+
.from('tasks')
|
|
85
|
+
.insert({
|
|
86
|
+
project_id: project.id,
|
|
87
|
+
title: input.title,
|
|
88
|
+
description: input.description ?? null,
|
|
89
|
+
priority: input.priority ?? 3,
|
|
90
|
+
skill_required: input.skill_required ?? null,
|
|
91
|
+
source_repo: input.source_repo ?? null,
|
|
92
|
+
status: 'backlog',
|
|
93
|
+
blocked_by: [],
|
|
94
|
+
sort_order: 0,
|
|
95
|
+
})
|
|
96
|
+
.select()
|
|
97
|
+
.single()
|
|
98
|
+
|
|
99
|
+
if (error) throw new Error(`Failed to create task: ${error.message}`)
|
|
100
|
+
return data as Task
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function updateTask(
|
|
104
|
+
taskId: string,
|
|
105
|
+
updates: Partial<Pick<Task, 'status' | 'assigned_to' | 'claimed_by' | 'claimed_at' | 'priority' | 'title' | 'description'>>
|
|
106
|
+
): Promise<Task> {
|
|
107
|
+
const updateData: Record<string, unknown> = { ...updates, updated_at: new Date().toISOString() }
|
|
108
|
+
|
|
109
|
+
// Handle completed_at
|
|
110
|
+
if (updates.status === 'done') {
|
|
111
|
+
updateData.completed_at = new Date().toISOString()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { data, error } = await sb()
|
|
115
|
+
.from('tasks')
|
|
116
|
+
.update(updateData)
|
|
117
|
+
.eq('id', taskId)
|
|
118
|
+
.select()
|
|
119
|
+
.single()
|
|
120
|
+
|
|
121
|
+
if (error) throw new Error(`Failed to update task ${taskId}: ${error.message}`)
|
|
122
|
+
return data as Task
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function getTask(taskId: string): Promise<Task> {
|
|
126
|
+
const { data, error } = await sb()
|
|
127
|
+
.from('tasks')
|
|
128
|
+
.select('*')
|
|
129
|
+
.eq('id', taskId)
|
|
130
|
+
.single()
|
|
131
|
+
|
|
132
|
+
if (error) throw new Error(`Task not found: ${taskId} — ${error.message}`)
|
|
133
|
+
return data as Task
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function claimTask(taskId: string, agentName: string): Promise<Task> {
|
|
137
|
+
const { data, error } = await sb()
|
|
138
|
+
.from('tasks')
|
|
139
|
+
.update({
|
|
140
|
+
claimed_by: agentName,
|
|
141
|
+
claimed_at: new Date().toISOString(),
|
|
142
|
+
status: 'in_progress',
|
|
143
|
+
updated_at: new Date().toISOString(),
|
|
144
|
+
})
|
|
145
|
+
.eq('id', taskId)
|
|
146
|
+
.select()
|
|
147
|
+
.single()
|
|
148
|
+
|
|
149
|
+
if (error) throw new Error(`Failed to claim task ${taskId}: ${error.message}`)
|
|
150
|
+
return data as Task
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function getNextTask(projectSlug: string, agentName: string): Promise<Task | null> {
|
|
154
|
+
const project = await getProjectBySlug(projectSlug)
|
|
155
|
+
|
|
156
|
+
const { data, error } = await sb()
|
|
157
|
+
.from('tasks')
|
|
158
|
+
.select('*')
|
|
159
|
+
.eq('project_id', project.id)
|
|
160
|
+
.in('status', ['backlog', 'ready'])
|
|
161
|
+
.is('claimed_by', null)
|
|
162
|
+
.order('priority', { ascending: true })
|
|
163
|
+
.order('created_at', { ascending: true })
|
|
164
|
+
.limit(10)
|
|
165
|
+
|
|
166
|
+
if (error) throw new Error(`Failed to fetch tasks: ${error.message}`)
|
|
167
|
+
if (!data || data.length === 0) return null
|
|
168
|
+
|
|
169
|
+
for (const task of data as Task[]) {
|
|
170
|
+
if (!task.blocked_by || task.blocked_by.length === 0) return task
|
|
171
|
+
const { data: blockers } = await sb()
|
|
172
|
+
.from('tasks')
|
|
173
|
+
.select('id, status')
|
|
174
|
+
.in('id', task.blocked_by)
|
|
175
|
+
const allDone = blockers?.every(b => b.status === 'done' || b.status === 'cancelled')
|
|
176
|
+
if (allDone) return task
|
|
177
|
+
}
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function getBoard(projectSlug: string): Promise<Task[]> {
|
|
182
|
+
const project = await getProjectBySlug(projectSlug)
|
|
183
|
+
|
|
184
|
+
const { data, error } = await sb()
|
|
185
|
+
.from('tasks')
|
|
186
|
+
.select('*')
|
|
187
|
+
.eq('project_id', project.id)
|
|
188
|
+
.not('status', 'eq', 'cancelled')
|
|
189
|
+
.order('sort_order')
|
|
190
|
+
.order('priority', { ascending: true })
|
|
191
|
+
.order('created_at', { ascending: true })
|
|
192
|
+
|
|
193
|
+
if (error) throw new Error(`Failed to fetch board: ${error.message}`)
|
|
194
|
+
return (data ?? []) as Task[]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function getBoardByStatus(projectSlug: string): Promise<Record<string, Task[]>> {
|
|
198
|
+
const tasks = await getBoard(projectSlug)
|
|
199
|
+
|
|
200
|
+
const byStatus: Record<string, Task[]> = {
|
|
201
|
+
backlog: [],
|
|
202
|
+
ready: [],
|
|
203
|
+
in_progress: [],
|
|
204
|
+
blocked: [],
|
|
205
|
+
review: [],
|
|
206
|
+
done: [],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const task of tasks) {
|
|
210
|
+
const status = task.status as string
|
|
211
|
+
if (byStatus[status]) {
|
|
212
|
+
byStatus[status].push(task)
|
|
213
|
+
} else {
|
|
214
|
+
byStatus.backlog.push(task)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return byStatus
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- Activity/Logs (if activity_log table exists) ---
|
|
222
|
+
|
|
223
|
+
export async function logActivity(
|
|
224
|
+
taskId: string,
|
|
225
|
+
entry: { agent: string; action: string; details?: string }
|
|
226
|
+
) {
|
|
227
|
+
const { error } = await sb()
|
|
228
|
+
.from('activity_log')
|
|
229
|
+
.insert({
|
|
230
|
+
task_id: taskId,
|
|
231
|
+
agent: entry.agent,
|
|
232
|
+
action: entry.action,
|
|
233
|
+
details: entry.details ?? null,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
if (error) {
|
|
237
|
+
console.warn(`Failed to log activity: ${error.message}`)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Obsidian Tasks Interface
|
|
3
|
+
*
|
|
4
|
+
* Reads/writes markdown task files in /home/oracle/Documents/optimal/tasks/
|
|
5
|
+
* Each task is a separate .md file with YAML frontmatter.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises'
|
|
8
|
+
import { existsSync } from 'node:fs'
|
|
9
|
+
import { join, basename } from 'node:path'
|
|
10
|
+
import { parse, stringify } from 'yaml'
|
|
11
|
+
|
|
12
|
+
const TASKS_DIR = process.env.OPTIMAL_TASKS_DIR || '/home/oracle/Documents/optimal/tasks'
|
|
13
|
+
|
|
14
|
+
export interface TaskFrontmatter {
|
|
15
|
+
id: string
|
|
16
|
+
type: 'task'
|
|
17
|
+
status: 'pending' | 'in-progress' | 'done' | 'blocked'
|
|
18
|
+
owner?: string
|
|
19
|
+
assignee?: string
|
|
20
|
+
project?: string
|
|
21
|
+
priority?: '1' | '2' | '3' | '4'
|
|
22
|
+
source?: string
|
|
23
|
+
created_at?: string
|
|
24
|
+
updated_at?: string
|
|
25
|
+
updated_by?: string
|
|
26
|
+
tags?: string[]
|
|
27
|
+
// Custom fields
|
|
28
|
+
title?: string
|
|
29
|
+
description?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Task {
|
|
33
|
+
id: string
|
|
34
|
+
frontmatter: TaskFrontmatter
|
|
35
|
+
body: string
|
|
36
|
+
filePath: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureTasksDir(): void {
|
|
40
|
+
if (!existsSync(TASKS_DIR)) {
|
|
41
|
+
throw new Error(`Tasks directory not found: ${TASKS_DIR}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function listTasks(filters?: {
|
|
46
|
+
project?: string
|
|
47
|
+
status?: string
|
|
48
|
+
owner?: string
|
|
49
|
+
assignee?: string
|
|
50
|
+
}): Promise<Task[]> {
|
|
51
|
+
ensureTasksDir()
|
|
52
|
+
|
|
53
|
+
const files = await readdir(TASKS_DIR)
|
|
54
|
+
const tasks: Task[] = []
|
|
55
|
+
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
if (!file.endsWith('.md') || file === 'index__tasks__task_hub.md') continue
|
|
58
|
+
|
|
59
|
+
const filePath = join(TASKS_DIR, file)
|
|
60
|
+
const content = await readFile(filePath, 'utf-8')
|
|
61
|
+
|
|
62
|
+
const task = parseTaskFile(content, filePath)
|
|
63
|
+
if (!task) continue
|
|
64
|
+
|
|
65
|
+
// Apply filters
|
|
66
|
+
if (filters?.project && task.frontmatter.project !== filters.project) continue
|
|
67
|
+
if (filters?.status && task.frontmatter.status !== filters.status) continue
|
|
68
|
+
if (filters?.owner && task.frontmatter.owner !== filters.owner) continue
|
|
69
|
+
if (filters?.assignee && task.frontmatter.assignee !== filters.assignee) continue
|
|
70
|
+
|
|
71
|
+
tasks.push(task)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return tasks.sort((a, b) => {
|
|
75
|
+
// Sort by priority (1 = highest), then by updated_at
|
|
76
|
+
const priA = parseInt(a.frontmatter.priority || '3')
|
|
77
|
+
const priB = parseInt(b.frontmatter.priority || '3')
|
|
78
|
+
if (priA !== priB) return priA - priB
|
|
79
|
+
return (b.frontmatter.updated_at || '').localeCompare(a.frontmatter.updated_at || '')
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function parseTaskFile(content: string, filePath: string): Task | null {
|
|
84
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
|
|
85
|
+
if (!match) return null
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const frontmatter = parse(match[1]) as TaskFrontmatter
|
|
89
|
+
const body = match[2] || ''
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
id: frontmatter.id || basename(filePath, '.md'),
|
|
93
|
+
frontmatter,
|
|
94
|
+
body,
|
|
95
|
+
filePath
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function getTaskById(id: string): Promise<Task | null> {
|
|
103
|
+
ensureTasksDir()
|
|
104
|
+
|
|
105
|
+
// Try to find by ID in filename
|
|
106
|
+
const files = await readdir(TASKS_DIR)
|
|
107
|
+
const matchingFile = files.find(f => f.includes(id))
|
|
108
|
+
|
|
109
|
+
if (!matchingFile) return null
|
|
110
|
+
|
|
111
|
+
const filePath = join(TASKS_DIR, matchingFile)
|
|
112
|
+
const content = await readFile(filePath, 'utf-8')
|
|
113
|
+
|
|
114
|
+
return parseTaskFile(content, filePath)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function createTask(opts: {
|
|
118
|
+
title: string
|
|
119
|
+
project?: string
|
|
120
|
+
description?: string
|
|
121
|
+
priority?: string
|
|
122
|
+
owner?: string
|
|
123
|
+
assignee?: string
|
|
124
|
+
source?: string
|
|
125
|
+
tags?: string[]
|
|
126
|
+
}): Promise<Task> {
|
|
127
|
+
ensureTasksDir()
|
|
128
|
+
|
|
129
|
+
const id = `task__${opts.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40)}_${Date.now().toString(36).slice(-6)}`
|
|
130
|
+
const now = new Date().toISOString()
|
|
131
|
+
|
|
132
|
+
const frontmatter: TaskFrontmatter = {
|
|
133
|
+
id,
|
|
134
|
+
type: 'task',
|
|
135
|
+
status: 'pending',
|
|
136
|
+
project: opts.project || 'default',
|
|
137
|
+
priority: (opts.priority as TaskFrontmatter['priority']) || '3',
|
|
138
|
+
owner: opts.owner,
|
|
139
|
+
assignee: opts.assignee,
|
|
140
|
+
source: opts.source,
|
|
141
|
+
created_at: now,
|
|
142
|
+
updated_at: now,
|
|
143
|
+
tags: opts.tags
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const content = `---
|
|
147
|
+
${stringify(frontmatter).trim()}
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
${opts.description || ''}
|
|
151
|
+
`
|
|
152
|
+
|
|
153
|
+
const filePath = join(TASKS_DIR, `${id}.md`)
|
|
154
|
+
await writeFile(filePath, content, 'utf-8')
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
id,
|
|
158
|
+
frontmatter,
|
|
159
|
+
body: opts.description || '',
|
|
160
|
+
filePath
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function updateTask(id: string, updates: Partial<{
|
|
165
|
+
status: TaskFrontmatter['status']
|
|
166
|
+
owner: string
|
|
167
|
+
assignee: string
|
|
168
|
+
priority: string
|
|
169
|
+
message: string
|
|
170
|
+
}>): Promise<Task | null> {
|
|
171
|
+
const task = await getTaskById(id)
|
|
172
|
+
if (!task) return null
|
|
173
|
+
|
|
174
|
+
const now = new Date().toISOString()
|
|
175
|
+
|
|
176
|
+
// Update frontmatter
|
|
177
|
+
if (updates.status) task.frontmatter.status = updates.status
|
|
178
|
+
if (updates.owner) task.frontmatter.owner = updates.owner
|
|
179
|
+
if (updates.assignee) task.frontmatter.assignee = updates.assignee
|
|
180
|
+
if (updates.priority) task.frontmatter.priority = updates.priority as TaskFrontmatter['priority']
|
|
181
|
+
task.frontmatter.updated_at = now
|
|
182
|
+
|
|
183
|
+
// Append message to body if provided
|
|
184
|
+
if (updates.message) {
|
|
185
|
+
const timestamp = new Date().toISOString()
|
|
186
|
+
task.body += `\n\n---\n*${timestamp}:* ${updates.message}`
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const content = `---
|
|
190
|
+
${stringify(task.frontmatter).trim()}
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
${task.body}
|
|
194
|
+
`
|
|
195
|
+
|
|
196
|
+
await writeFile(task.filePath, content, 'utf-8')
|
|
197
|
+
|
|
198
|
+
return task
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function deleteTask(id: string): Promise<boolean> {
|
|
202
|
+
const task = await getTaskById(id)
|
|
203
|
+
if (!task) return false
|
|
204
|
+
|
|
205
|
+
const { rm } = await import('node:fs/promises')
|
|
206
|
+
await rm(task.filePath)
|
|
207
|
+
|
|
208
|
+
return true
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function getTasksDir(): string {
|
|
212
|
+
return TASKS_DIR
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Group tasks by status for board view
|
|
216
|
+
export function groupTasksByStatus(tasks: Task[]): Record<string, Task[]> {
|
|
217
|
+
const groups: Record<string, Task[]> = {
|
|
218
|
+
pending: [],
|
|
219
|
+
'in-progress': [],
|
|
220
|
+
blocked: [],
|
|
221
|
+
done: []
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const task of tasks) {
|
|
225
|
+
const status = task.frontmatter.status || 'pending'
|
|
226
|
+
if (!groups[status]) groups[status] = []
|
|
227
|
+
groups[status].push(task)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return groups
|
|
231
|
+
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "optimal-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Optimal CLI — unified command-line interface for bot orchestration, financial analytics, content management, and infrastructure",
|
|
3
|
+
"version": "1.1.0",
|
|
5
4
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"optimal": "./bin/optimal.ts"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"bin/",
|
|
11
|
-
"lib/",
|
|
12
|
-
"agents/",
|
|
13
|
-
"docs/CLI-REFERENCE.md"
|
|
14
|
-
],
|
|
15
|
-
"keywords": ["cli", "supabase", "kanban", "bot-orchestration", "financial-analytics"],
|
|
16
|
-
"author": "Carlos Lenis <clenis@users.noreply.github.com>",
|
|
17
|
-
"license": "MIT",
|
|
18
|
-
"repository": {
|
|
19
|
-
"type": "git",
|
|
20
|
-
"url": "https://github.com/clenisa/optimal-cli.git"
|
|
21
|
-
},
|
|
22
5
|
"scripts": {
|
|
23
6
|
"build": "tsc",
|
|
24
7
|
"dev": "tsx watch bin/optimal.ts",
|
|
@@ -32,9 +15,12 @@
|
|
|
32
15
|
},
|
|
33
16
|
"dependencies": {
|
|
34
17
|
"@supabase/supabase-js": "^2.49.0",
|
|
18
|
+
"@types/glob": "^9.0.0",
|
|
35
19
|
"commander": "^13.0.0",
|
|
36
20
|
"dotenv": "^16.4.0",
|
|
37
21
|
"exceljs": "^4.4.0",
|
|
38
|
-
"
|
|
22
|
+
"glob": "^13.0.6",
|
|
23
|
+
"playwright": "^1.58.2",
|
|
24
|
+
"yaml": "^2.8.2"
|
|
39
25
|
}
|
|
40
26
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js'
|
|
2
|
+
|
|
3
|
+
const url = 'https://hbfalrpswysryltysonm.supabase.co'
|
|
4
|
+
const key = process.env.OPTIMAL_SUPABASE_SERVICE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhiZmFscnBzd3lzcnlsdHlzb25tIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTczODI0NTY0NywiZXhwIjoyMDUzODIxNjQ3fQ.placeholder'
|
|
5
|
+
|
|
6
|
+
const supabase = createClient(url, key)
|
|
7
|
+
|
|
8
|
+
async function checkTable() {
|
|
9
|
+
// Try to query the table
|
|
10
|
+
const { data, error } = await supabase
|
|
11
|
+
.from('agent_configs')
|
|
12
|
+
.select('*')
|
|
13
|
+
.limit(1)
|
|
14
|
+
|
|
15
|
+
if (error) {
|
|
16
|
+
console.log('Table status:', error.code === '42P01' ? 'DOES NOT EXIST' : 'ERROR')
|
|
17
|
+
console.log('Error:', error.message)
|
|
18
|
+
} else {
|
|
19
|
+
console.log('Table status: EXISTS')
|
|
20
|
+
console.log('Rows:', data.length)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
checkTable()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run kanban migration - creates tables in supabase
|
|
3
|
+
*/
|
|
4
|
+
import { createClient } from '@supabase/supabase-js'
|
|
5
|
+
|
|
6
|
+
const SUPABASE_URL = 'https://vvutttwunexshxkmygik.supabase.co'
|
|
7
|
+
const SUPABASE_SERVICE_KEY = 'sb_secret_zokm_eFGX9ENuRV8hKA05Q_JQDOEOOl'
|
|
8
|
+
|
|
9
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY, {
|
|
10
|
+
db: { schema: 'public' }
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const sql = `
|
|
14
|
+
-- Projects table
|
|
15
|
+
CREATE TABLE IF NOT EXISTS cli_projects (
|
|
16
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
17
|
+
slug TEXT UNIQUE NOT NULL,
|
|
18
|
+
name TEXT NOT NULL,
|
|
19
|
+
description TEXT,
|
|
20
|
+
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'archived', 'on_hold')),
|
|
21
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
22
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
-- Tasks table
|
|
26
|
+
CREATE TABLE IF NOT EXISTS cli_tasks (
|
|
27
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
28
|
+
project_id UUID REFERENCES cli_projects(id) ON DELETE CASCADE,
|
|
29
|
+
task_id TEXT NOT NULL,
|
|
30
|
+
title TEXT NOT NULL,
|
|
31
|
+
description TEXT,
|
|
32
|
+
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'done', 'cancelled')),
|
|
33
|
+
priority INTEGER DEFAULT 3 CHECK (priority BETWEEN 1 AND 4),
|
|
34
|
+
owner TEXT,
|
|
35
|
+
assignee TEXT,
|
|
36
|
+
tags JSONB DEFAULT '[]',
|
|
37
|
+
source TEXT,
|
|
38
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
39
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
40
|
+
completed_at TIMESTAMPTZ,
|
|
41
|
+
completed_by TEXT,
|
|
42
|
+
metadata JSONB DEFAULT '{}',
|
|
43
|
+
UNIQUE(project_id, task_id)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
-- Sync log
|
|
47
|
+
CREATE TABLE IF NOT EXISTS cli_sync_log (
|
|
48
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
49
|
+
entity_type TEXT NOT NULL,
|
|
50
|
+
entity_id UUID NOT NULL,
|
|
51
|
+
action TEXT NOT NULL,
|
|
52
|
+
source TEXT NOT NULL,
|
|
53
|
+
synced_at TIMESTAMPTZ DEFAULT NOW(),
|
|
54
|
+
payload JSONB
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
-- Indexes
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project ON cli_tasks(project_id);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON cli_tasks(status);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_owner ON cli_tasks(owner);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON cli_tasks(assignee);
|
|
62
|
+
`
|
|
63
|
+
|
|
64
|
+
async function main() {
|
|
65
|
+
console.log('Running migration...')
|
|
66
|
+
|
|
67
|
+
// Run raw SQL via postgrest
|
|
68
|
+
const { data, error } = await supabase.rpc('pg_catalog.exec', {
|
|
69
|
+
query: sql
|
|
70
|
+
}).catch(() => {
|
|
71
|
+
// Fallback: try via REST directly
|
|
72
|
+
return supabase.from('_temp').select('*').limit(0)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Actually, rpc won't work - let's just create tables one by one via insert
|
|
76
|
+
// Since we can't run raw SQL easily, let's try a different approach
|
|
77
|
+
console.log('Trying alternative...')
|
|
78
|
+
|
|
79
|
+
// Check if tables exist
|
|
80
|
+
const { data: tables } = await supabase
|
|
81
|
+
.from('information_schema.tables')
|
|
82
|
+
.select('table_name')
|
|
83
|
+
.eq('table_schema', 'public')
|
|
84
|
+
.in('table_name', ['cli_projects', 'cli_tasks'])
|
|
85
|
+
|
|
86
|
+
console.log('Existing tables:', tables)
|
|
87
|
+
|
|
88
|
+
if (!tables?.length) {
|
|
89
|
+
console.log('Tables do not exist. Trying to create via console...')
|
|
90
|
+
console.log('Please run manually or use supabase studio.')
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main().catch(console.error)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Migrate kanban tables to supabase
|
|
3
|
+
|
|
4
|
+
SQL_FILE="supabase/migrations/20260306195000_create_kanban_tables.sql"
|
|
5
|
+
|
|
6
|
+
# Load env from workspace secrets
|
|
7
|
+
if [ -f ".secrets/dashboard-returnpro.vercel.env" ]; then
|
|
8
|
+
export "$(cat .secrets/dashboard-returnpro.vercel.env | grep -v '^#' | xargs)"
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
# Check for supabase URL and key
|
|
12
|
+
if [ -z "$SUPABASE_URL" ] || [ -z "$SUPABASE_SERVICE_ROLE_KEY" ]; then
|
|
13
|
+
echo "Error: SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY not found"
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
echo "Applying migration..."
|
|
18
|
+
echo "SUPABASE_URL: $SUPABASE_URL"
|
|
19
|
+
|
|
20
|
+
# Use psql if available, otherwise try with supabase cli
|
|
21
|
+
if command -v psql &> /dev/null; then
|
|
22
|
+
# Extract password from the key (it's after the = sign)
|
|
23
|
+
DB_PASS="${SUPABASE_SERVICE_ROLE_KEY##*=}"
|
|
24
|
+
psql "host=vvutttwunexshxkmygik.supabase.co port=5432 dbname=postgres user=postgres password=$DB_PASS" -f "$SQL_FILE" 2>&1
|
|
25
|
+
else
|
|
26
|
+
echo "psql not available, trying supabase cli..."
|
|
27
|
+
npx supabase db push 2>&1
|
|
28
|
+
fi
|