optimal-cli 0.1.0 → 1.0.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/agents/.gitkeep +0 -0
- package/agents/content-ops.md +227 -0
- package/agents/financial-ops.md +184 -0
- package/agents/infra-ops.md +206 -0
- package/agents/profiles.json +5 -0
- package/dist/bin/optimal.d.ts +1 -1
- package/dist/bin/optimal.js +706 -111
- package/dist/lib/assets/index.d.ts +79 -0
- package/dist/lib/assets/index.js +153 -0
- package/dist/lib/assets.d.ts +20 -0
- package/dist/lib/assets.js +112 -0
- package/dist/lib/auth/index.d.ts +83 -0
- package/dist/lib/auth/index.js +146 -0
- package/dist/lib/board/index.d.ts +39 -0
- package/dist/lib/board/index.js +285 -0
- package/dist/lib/board/types.d.ts +111 -0
- package/dist/lib/board/types.js +1 -0
- package/dist/lib/bot/claim.d.ts +3 -0
- package/dist/lib/bot/claim.js +20 -0
- package/dist/lib/bot/coordinator.d.ts +27 -0
- package/dist/lib/bot/coordinator.js +178 -0
- package/dist/lib/bot/heartbeat.d.ts +6 -0
- package/dist/lib/bot/heartbeat.js +30 -0
- package/dist/lib/bot/index.d.ts +9 -0
- package/dist/lib/bot/index.js +6 -0
- package/dist/lib/bot/protocol.d.ts +12 -0
- package/dist/lib/bot/protocol.js +74 -0
- package/dist/lib/bot/reporter.d.ts +3 -0
- package/dist/lib/bot/reporter.js +27 -0
- package/dist/lib/bot/skills.d.ts +26 -0
- package/dist/lib/bot/skills.js +69 -0
- package/dist/lib/config/registry.d.ts +17 -0
- package/dist/lib/config/registry.js +182 -0
- package/dist/lib/config/schema.d.ts +31 -0
- package/dist/lib/config/schema.js +25 -0
- package/dist/lib/errors.d.ts +25 -0
- package/dist/lib/errors.js +91 -0
- package/dist/lib/format.d.ts +28 -0
- package/dist/lib/format.js +98 -0
- package/dist/lib/returnpro/validate.d.ts +37 -0
- package/dist/lib/returnpro/validate.js +124 -0
- package/dist/lib/social/meta.d.ts +90 -0
- package/dist/lib/social/meta.js +160 -0
- package/docs/CLI-REFERENCE.md +361 -0
- package/package.json +13 -24
- package/dist/lib/kanban.d.ts +0 -46
- package/dist/lib/kanban.js +0 -118
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset tracking — manage digital infrastructure items
|
|
3
|
+
* (domains, servers, API keys, services, repos).
|
|
4
|
+
* Stores in the OptimalOS Supabase instance.
|
|
5
|
+
*/
|
|
6
|
+
export type AssetType = 'domain' | 'server' | 'api_key' | 'service' | 'repo' | 'other';
|
|
7
|
+
export type AssetStatus = 'active' | 'inactive' | 'expired' | 'pending';
|
|
8
|
+
export interface Asset {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
type: AssetType;
|
|
12
|
+
status: AssetStatus;
|
|
13
|
+
metadata: Record<string, unknown>;
|
|
14
|
+
owner: string | null;
|
|
15
|
+
expires_at: string | null;
|
|
16
|
+
created_at: string;
|
|
17
|
+
updated_at: string;
|
|
18
|
+
}
|
|
19
|
+
export interface CreateAssetInput {
|
|
20
|
+
name: string;
|
|
21
|
+
type: AssetType;
|
|
22
|
+
status?: AssetStatus;
|
|
23
|
+
metadata?: Record<string, unknown>;
|
|
24
|
+
owner?: string;
|
|
25
|
+
expires_at?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface UpdateAssetInput {
|
|
28
|
+
name?: string;
|
|
29
|
+
type?: AssetType;
|
|
30
|
+
status?: AssetStatus;
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
owner?: string;
|
|
33
|
+
expires_at?: string | null;
|
|
34
|
+
}
|
|
35
|
+
export interface AssetFilters {
|
|
36
|
+
type?: AssetType;
|
|
37
|
+
status?: AssetStatus;
|
|
38
|
+
owner?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface AssetUsageEvent {
|
|
41
|
+
id: string;
|
|
42
|
+
asset_id: string;
|
|
43
|
+
event: string;
|
|
44
|
+
actor: string | null;
|
|
45
|
+
metadata: Record<string, unknown>;
|
|
46
|
+
created_at: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* List assets, optionally filtered by type, status, or owner.
|
|
50
|
+
*/
|
|
51
|
+
export declare function listAssets(filters?: AssetFilters): Promise<Asset[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Create a new asset.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createAsset(input: CreateAssetInput): Promise<Asset>;
|
|
56
|
+
/**
|
|
57
|
+
* Update an existing asset by ID.
|
|
58
|
+
*/
|
|
59
|
+
export declare function updateAsset(id: string, updates: UpdateAssetInput): Promise<Asset>;
|
|
60
|
+
/**
|
|
61
|
+
* Get a single asset by ID.
|
|
62
|
+
*/
|
|
63
|
+
export declare function getAsset(id: string): Promise<Asset>;
|
|
64
|
+
/**
|
|
65
|
+
* Delete an asset by ID.
|
|
66
|
+
*/
|
|
67
|
+
export declare function deleteAsset(id: string): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Log a usage event against an asset.
|
|
70
|
+
*/
|
|
71
|
+
export declare function trackAssetUsage(assetId: string, event: string, actor?: string, metadata?: Record<string, unknown>): Promise<AssetUsageEvent>;
|
|
72
|
+
/**
|
|
73
|
+
* List usage events for a given asset.
|
|
74
|
+
*/
|
|
75
|
+
export declare function listAssetUsage(assetId: string, limit?: number): Promise<AssetUsageEvent[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Format assets into a table string for CLI display.
|
|
78
|
+
*/
|
|
79
|
+
export declare function formatAssetTable(assets: Asset[]): string;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset tracking — manage digital infrastructure items
|
|
3
|
+
* (domains, servers, API keys, services, repos).
|
|
4
|
+
* Stores in the OptimalOS Supabase instance.
|
|
5
|
+
*/
|
|
6
|
+
import { getSupabase } from '../supabase.js';
|
|
7
|
+
// ── Supabase accessor ────────────────────────────────────────────────
|
|
8
|
+
const sb = () => getSupabase('optimal');
|
|
9
|
+
// ── CRUD operations ──────────────────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* List assets, optionally filtered by type, status, or owner.
|
|
12
|
+
*/
|
|
13
|
+
export async function listAssets(filters) {
|
|
14
|
+
let query = sb().from('assets').select('*');
|
|
15
|
+
if (filters?.type)
|
|
16
|
+
query = query.eq('type', filters.type);
|
|
17
|
+
if (filters?.status)
|
|
18
|
+
query = query.eq('status', filters.status);
|
|
19
|
+
if (filters?.owner)
|
|
20
|
+
query = query.eq('owner', filters.owner);
|
|
21
|
+
const { data, error } = await query.order('updated_at', { ascending: false });
|
|
22
|
+
if (error)
|
|
23
|
+
throw new Error(`Failed to list assets: ${error.message}`);
|
|
24
|
+
return (data ?? []);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create a new asset.
|
|
28
|
+
*/
|
|
29
|
+
export async function createAsset(input) {
|
|
30
|
+
const { data, error } = await sb()
|
|
31
|
+
.from('assets')
|
|
32
|
+
.insert({
|
|
33
|
+
name: input.name,
|
|
34
|
+
type: input.type,
|
|
35
|
+
status: input.status ?? 'active',
|
|
36
|
+
metadata: input.metadata ?? {},
|
|
37
|
+
owner: input.owner ?? null,
|
|
38
|
+
expires_at: input.expires_at ?? null,
|
|
39
|
+
})
|
|
40
|
+
.select()
|
|
41
|
+
.single();
|
|
42
|
+
if (error)
|
|
43
|
+
throw new Error(`Failed to create asset: ${error.message}`);
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Update an existing asset by ID.
|
|
48
|
+
*/
|
|
49
|
+
export async function updateAsset(id, updates) {
|
|
50
|
+
const { data, error } = await sb()
|
|
51
|
+
.from('assets')
|
|
52
|
+
.update(updates)
|
|
53
|
+
.eq('id', id)
|
|
54
|
+
.select()
|
|
55
|
+
.single();
|
|
56
|
+
if (error)
|
|
57
|
+
throw new Error(`Failed to update asset: ${error.message}`);
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get a single asset by ID.
|
|
62
|
+
*/
|
|
63
|
+
export async function getAsset(id) {
|
|
64
|
+
const { data, error } = await sb()
|
|
65
|
+
.from('assets')
|
|
66
|
+
.select('*')
|
|
67
|
+
.eq('id', id)
|
|
68
|
+
.single();
|
|
69
|
+
if (error)
|
|
70
|
+
throw new Error(`Asset not found: ${error.message}`);
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Delete an asset by ID.
|
|
75
|
+
*/
|
|
76
|
+
export async function deleteAsset(id) {
|
|
77
|
+
const { error } = await sb()
|
|
78
|
+
.from('assets')
|
|
79
|
+
.delete()
|
|
80
|
+
.eq('id', id);
|
|
81
|
+
if (error)
|
|
82
|
+
throw new Error(`Failed to delete asset: ${error.message}`);
|
|
83
|
+
}
|
|
84
|
+
// ── Usage tracking ───────────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Log a usage event against an asset.
|
|
87
|
+
*/
|
|
88
|
+
export async function trackAssetUsage(assetId, event, actor, metadata) {
|
|
89
|
+
const { data, error } = await sb()
|
|
90
|
+
.from('asset_usage_log')
|
|
91
|
+
.insert({
|
|
92
|
+
asset_id: assetId,
|
|
93
|
+
event,
|
|
94
|
+
actor: actor ?? null,
|
|
95
|
+
metadata: metadata ?? {},
|
|
96
|
+
})
|
|
97
|
+
.select()
|
|
98
|
+
.single();
|
|
99
|
+
if (error)
|
|
100
|
+
throw new Error(`Failed to track usage: ${error.message}`);
|
|
101
|
+
return data;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* List usage events for a given asset.
|
|
105
|
+
*/
|
|
106
|
+
export async function listAssetUsage(assetId, limit = 50) {
|
|
107
|
+
const { data, error } = await sb()
|
|
108
|
+
.from('asset_usage_log')
|
|
109
|
+
.select('*')
|
|
110
|
+
.eq('asset_id', assetId)
|
|
111
|
+
.order('created_at', { ascending: false })
|
|
112
|
+
.limit(limit);
|
|
113
|
+
if (error)
|
|
114
|
+
throw new Error(`Failed to list usage: ${error.message}`);
|
|
115
|
+
return (data ?? []);
|
|
116
|
+
}
|
|
117
|
+
// ── Formatting ───────────────────────────────────────────────────────
|
|
118
|
+
const TYPE_LABELS = {
|
|
119
|
+
domain: 'Domain',
|
|
120
|
+
server: 'Server',
|
|
121
|
+
api_key: 'API Key',
|
|
122
|
+
service: 'Service',
|
|
123
|
+
repo: 'Repo',
|
|
124
|
+
other: 'Other',
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Format assets into a table string for CLI display.
|
|
128
|
+
*/
|
|
129
|
+
export function formatAssetTable(assets) {
|
|
130
|
+
if (assets.length === 0)
|
|
131
|
+
return 'No assets found.';
|
|
132
|
+
const headers = ['Type', 'Status', 'Name', 'Owner', 'Expires'];
|
|
133
|
+
const rows = assets.map(a => [
|
|
134
|
+
TYPE_LABELS[a.type] ?? a.type,
|
|
135
|
+
a.status,
|
|
136
|
+
a.name.length > 35 ? a.name.slice(0, 32) + '...' : a.name,
|
|
137
|
+
a.owner ?? '-',
|
|
138
|
+
a.expires_at ? a.expires_at.slice(0, 10) : '-',
|
|
139
|
+
]);
|
|
140
|
+
// Compute column widths
|
|
141
|
+
const widths = headers.map((h, i) => {
|
|
142
|
+
let max = h.length;
|
|
143
|
+
for (const row of rows) {
|
|
144
|
+
if ((row[i]?.length ?? 0) > max)
|
|
145
|
+
max = row[i].length;
|
|
146
|
+
}
|
|
147
|
+
return max;
|
|
148
|
+
});
|
|
149
|
+
const sep = '+-' + widths.map(w => '-'.repeat(w)).join('-+-') + '-+';
|
|
150
|
+
const headerRow = '| ' + headers.map((h, i) => h.padEnd(widths[i])).join(' | ') + ' |';
|
|
151
|
+
const bodyRows = rows.map(row => '| ' + row.map((cell, i) => (cell ?? '').padEnd(widths[i])).join(' | ') + ' |');
|
|
152
|
+
return [sep, headerRow, sep, ...bodyRows, sep, `\nTotal: ${assets.length} assets`].join('\n');
|
|
153
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ScannedAsset {
|
|
2
|
+
type: 'skill' | 'cli' | 'cron' | 'repo' | 'env' | 'ssh_key' | 'plugin';
|
|
3
|
+
name: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
path: string;
|
|
6
|
+
hash: string;
|
|
7
|
+
content?: string;
|
|
8
|
+
metadata: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export declare function scanSkills(): ScannedAsset[];
|
|
11
|
+
export declare function scanPlugins(): ScannedAsset[];
|
|
12
|
+
export declare function scanCLIs(): ScannedAsset[];
|
|
13
|
+
export declare function scanRepos(): ScannedAsset[];
|
|
14
|
+
export declare function scanAllAssets(): ScannedAsset[];
|
|
15
|
+
export declare function pushAssets(agentName: string): Promise<{
|
|
16
|
+
pushed: number;
|
|
17
|
+
updated: number;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function listAssets(agentName?: string): Promise<any[]>;
|
|
20
|
+
export declare function getInventory(): Promise<any[]>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
const CONFIG_DIR = join(homedir(), '.openclaw');
|
|
7
|
+
const SKILLS_DIR = join(CONFIG_DIR, 'skills');
|
|
8
|
+
const PLUGINS_DIR = join(CONFIG_DIR, 'plugins');
|
|
9
|
+
const WORKSPACE_DIR = join(homedir(), '.openclaw', 'workspace');
|
|
10
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
11
|
+
function getSupabase() {
|
|
12
|
+
const url = process.env.OPTIMAL_SUPABASE_URL;
|
|
13
|
+
const key = process.env.OPTIMAL_SUPABASE_SERVICE_KEY;
|
|
14
|
+
if (!url || !key)
|
|
15
|
+
throw new Error('OPTIMAL_SUPABASE_URL and OPTIMAL_SUPABASE_SERVICE_KEY required');
|
|
16
|
+
return createClient(url, key);
|
|
17
|
+
}
|
|
18
|
+
function hashContent(content) {
|
|
19
|
+
return createHash('sha256').update(content).digest('hex');
|
|
20
|
+
}
|
|
21
|
+
export function scanSkills() {
|
|
22
|
+
const assets = [];
|
|
23
|
+
if (!existsSync(SKILLS_DIR))
|
|
24
|
+
return assets;
|
|
25
|
+
for (const dir of readdirSync(SKILLS_DIR)) {
|
|
26
|
+
const skillPath = join(SKILLS_DIR, dir);
|
|
27
|
+
if (!statSync(skillPath).isDirectory())
|
|
28
|
+
continue;
|
|
29
|
+
const skillFile = join(skillPath, 'SKILL.md');
|
|
30
|
+
if (existsSync(skillFile)) {
|
|
31
|
+
const content = readFileSync(skillFile, 'utf-8');
|
|
32
|
+
assets.push({ type: 'skill', name: dir, path: skillFile, hash: hashContent(content), content, metadata: {} });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return assets;
|
|
36
|
+
}
|
|
37
|
+
export function scanPlugins() {
|
|
38
|
+
const assets = [];
|
|
39
|
+
if (!existsSync(PLUGINS_DIR))
|
|
40
|
+
return assets;
|
|
41
|
+
for (const dir of readdirSync(PLUGINS_DIR)) {
|
|
42
|
+
const pluginPath = join(PLUGINS_DIR, dir);
|
|
43
|
+
if (!statSync(pluginPath).isDirectory())
|
|
44
|
+
continue;
|
|
45
|
+
assets.push({ type: 'plugin', name: dir, path: pluginPath, hash: hashContent(dir), metadata: {} });
|
|
46
|
+
}
|
|
47
|
+
return assets;
|
|
48
|
+
}
|
|
49
|
+
export function scanCLIs() {
|
|
50
|
+
const assets = [];
|
|
51
|
+
const knownCLIs = ['vercel', 'supabase', 'gh', 'openclaw'];
|
|
52
|
+
for (const cli of knownCLIs) {
|
|
53
|
+
try {
|
|
54
|
+
const { execSync } = require('node:child_process');
|
|
55
|
+
const version = execSync(`${cli} --version 2>/dev/null || echo ""`).toString().trim();
|
|
56
|
+
if (version) {
|
|
57
|
+
assets.push({ type: 'cli', name: cli, version: version.slice(0, 20), path: '', hash: hashContent(version), metadata: {} });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
}
|
|
62
|
+
return assets;
|
|
63
|
+
}
|
|
64
|
+
export function scanRepos() {
|
|
65
|
+
const assets = [];
|
|
66
|
+
if (!existsSync(WORKSPACE_DIR))
|
|
67
|
+
return assets;
|
|
68
|
+
for (const dir of readdirSync(WORKSPACE_DIR)) {
|
|
69
|
+
const repoPath = join(WORKSPACE_DIR, dir);
|
|
70
|
+
if (!statSync(repoPath).isDirectory())
|
|
71
|
+
continue;
|
|
72
|
+
if (existsSync(join(repoPath, '.git'))) {
|
|
73
|
+
assets.push({ type: 'repo', name: dir, path: repoPath, hash: '', metadata: {} });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return assets;
|
|
77
|
+
}
|
|
78
|
+
export function scanAllAssets() {
|
|
79
|
+
return [...scanSkills(), ...scanPlugins(), ...scanCLIs(), ...scanRepos()];
|
|
80
|
+
}
|
|
81
|
+
export async function pushAssets(agentName) {
|
|
82
|
+
const supabase = getSupabase();
|
|
83
|
+
const assets = scanAllAssets();
|
|
84
|
+
let pushed = 0, updated = 0;
|
|
85
|
+
for (const asset of assets) {
|
|
86
|
+
const { data: existing } = await supabase.from('agent_assets').select('id, asset_hash').eq('agent_name', agentName).eq('asset_type', asset.type).eq('asset_name', asset.name).single();
|
|
87
|
+
if (existing) {
|
|
88
|
+
if (existing.asset_hash !== asset.hash) {
|
|
89
|
+
await supabase.from('agent_assets').update({ asset_version: asset.version, asset_path: asset.path, asset_hash: asset.hash, content: asset.content, metadata: asset.metadata, updated_at: new Date().toISOString() }).eq('id', existing.id);
|
|
90
|
+
updated++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
await supabase.from('agent_assets').insert({ agent_name: agentName, asset_type: asset.type, asset_name: asset.name, asset_version: asset.version, asset_path: asset.path, asset_hash: asset.hash, content: asset.content, metadata: asset.metadata });
|
|
95
|
+
pushed++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { pushed, updated };
|
|
99
|
+
}
|
|
100
|
+
export async function listAssets(agentName) {
|
|
101
|
+
const supabase = getSupabase();
|
|
102
|
+
let query = supabase.from('agent_assets').select('*');
|
|
103
|
+
if (agentName)
|
|
104
|
+
query = query.eq('agent_name', agentName);
|
|
105
|
+
const { data } = await query.order('updated_at', { ascending: false });
|
|
106
|
+
return data || [];
|
|
107
|
+
}
|
|
108
|
+
export async function getInventory() {
|
|
109
|
+
const supabase = getSupabase();
|
|
110
|
+
const { data } = await supabase.from('agent_inventory').select('*');
|
|
111
|
+
return data || [];
|
|
112
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth module — ported from optimalOS Supabase auth patterns.
|
|
3
|
+
*
|
|
4
|
+
* OptimalOS uses three client tiers:
|
|
5
|
+
* 1. Browser client (anon key + cookie session) — N/A for CLI
|
|
6
|
+
* 2. Server client (anon key + SSR cookies) — N/A for CLI
|
|
7
|
+
* 3. Admin client (service_role key, no session) — primary CLI path
|
|
8
|
+
*
|
|
9
|
+
* In a headless CLI context there are no cookies or browser sessions.
|
|
10
|
+
* Auth reduces to two modes:
|
|
11
|
+
* - Service-role access (bot / automation operations)
|
|
12
|
+
* - User-scoped access (pass an access_token obtained externally)
|
|
13
|
+
*
|
|
14
|
+
* Environment variables (defined in .env):
|
|
15
|
+
* OPTIMAL_SUPABASE_URL — Supabase project URL
|
|
16
|
+
* OPTIMAL_SUPABASE_SERVICE_KEY — service_role secret
|
|
17
|
+
*/
|
|
18
|
+
import { type SupabaseClient } from '@supabase/supabase-js';
|
|
19
|
+
import 'dotenv/config';
|
|
20
|
+
/** Describes how the current invocation is authenticated. */
|
|
21
|
+
export interface AuthContext {
|
|
22
|
+
/** 'service' when using service_role key, 'user' when using a user JWT */
|
|
23
|
+
mode: 'service' | 'user';
|
|
24
|
+
/** The Supabase client for this context */
|
|
25
|
+
client: SupabaseClient;
|
|
26
|
+
/** User ID (only set when mode === 'user') */
|
|
27
|
+
userId?: string;
|
|
28
|
+
/** User email (only set when mode === 'user' and resolvable) */
|
|
29
|
+
email?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Minimal session shape returned by getSession(). */
|
|
32
|
+
export interface Session {
|
|
33
|
+
accessToken: string;
|
|
34
|
+
user: {
|
|
35
|
+
id: string;
|
|
36
|
+
email?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Return a service-role Supabase client.
|
|
41
|
+
*
|
|
42
|
+
* Mirrors optimalOS `createAdminClient()` from lib/supabase/admin.ts:
|
|
43
|
+
* - Uses SUPABASE_SERVICE_ROLE_KEY
|
|
44
|
+
* - persistSession: false, autoRefreshToken: false
|
|
45
|
+
* - Singleton — safe to call repeatedly
|
|
46
|
+
*/
|
|
47
|
+
export declare function getServiceClient(): SupabaseClient;
|
|
48
|
+
/**
|
|
49
|
+
* Return a user-scoped Supabase client authenticated with the given JWT.
|
|
50
|
+
*
|
|
51
|
+
* This is the CLI equivalent of optimalOS browser/server clients that carry
|
|
52
|
+
* a user session via cookies. The caller is responsible for obtaining the
|
|
53
|
+
* access token (e.g., via `supabase login`, OAuth device flow, or env var).
|
|
54
|
+
*
|
|
55
|
+
* A new client is created on every call — callers should cache if needed.
|
|
56
|
+
*/
|
|
57
|
+
export declare function getUserClient(accessToken: string): SupabaseClient;
|
|
58
|
+
/**
|
|
59
|
+
* Attempt to retrieve the current session.
|
|
60
|
+
*
|
|
61
|
+
* In the CLI there is no implicit cookie jar. A session exists only when:
|
|
62
|
+
* 1. OPTIMAL_ACCESS_TOKEN env var is set (user JWT), or
|
|
63
|
+
* 2. A future `optimal login` command has cached a token locally.
|
|
64
|
+
*
|
|
65
|
+
* Returns null if no user session is available (service-role only mode).
|
|
66
|
+
*/
|
|
67
|
+
export declare function getSession(): Promise<Session | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Guard that throws if no user session is present.
|
|
70
|
+
*
|
|
71
|
+
* Use at the top of CLI commands that require a logged-in user:
|
|
72
|
+
*
|
|
73
|
+
* const session = await requireAuth()
|
|
74
|
+
* // session.user.id is guaranteed
|
|
75
|
+
*/
|
|
76
|
+
export declare function requireAuth(): Promise<Session>;
|
|
77
|
+
/**
|
|
78
|
+
* Build an AuthContext describing the current invocation's auth state.
|
|
79
|
+
*
|
|
80
|
+
* Prefers user-scoped auth when OPTIMAL_ACCESS_TOKEN is set;
|
|
81
|
+
* falls back to service-role.
|
|
82
|
+
*/
|
|
83
|
+
export declare function resolveAuthContext(): Promise<AuthContext>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth module — ported from optimalOS Supabase auth patterns.
|
|
3
|
+
*
|
|
4
|
+
* OptimalOS uses three client tiers:
|
|
5
|
+
* 1. Browser client (anon key + cookie session) — N/A for CLI
|
|
6
|
+
* 2. Server client (anon key + SSR cookies) — N/A for CLI
|
|
7
|
+
* 3. Admin client (service_role key, no session) — primary CLI path
|
|
8
|
+
*
|
|
9
|
+
* In a headless CLI context there are no cookies or browser sessions.
|
|
10
|
+
* Auth reduces to two modes:
|
|
11
|
+
* - Service-role access (bot / automation operations)
|
|
12
|
+
* - User-scoped access (pass an access_token obtained externally)
|
|
13
|
+
*
|
|
14
|
+
* Environment variables (defined in .env):
|
|
15
|
+
* OPTIMAL_SUPABASE_URL — Supabase project URL
|
|
16
|
+
* OPTIMAL_SUPABASE_SERVICE_KEY — service_role secret
|
|
17
|
+
*/
|
|
18
|
+
import { createClient } from '@supabase/supabase-js';
|
|
19
|
+
import 'dotenv/config';
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Internal helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
function envOrThrow(name) {
|
|
24
|
+
const value = process.env[name];
|
|
25
|
+
if (!value) {
|
|
26
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
/** Singleton service-role client (matches optimalOS admin.ts pattern). */
|
|
31
|
+
let _serviceClient = null;
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Public API
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
/**
|
|
36
|
+
* Return a service-role Supabase client.
|
|
37
|
+
*
|
|
38
|
+
* Mirrors optimalOS `createAdminClient()` from lib/supabase/admin.ts:
|
|
39
|
+
* - Uses SUPABASE_SERVICE_ROLE_KEY
|
|
40
|
+
* - persistSession: false, autoRefreshToken: false
|
|
41
|
+
* - Singleton — safe to call repeatedly
|
|
42
|
+
*/
|
|
43
|
+
export function getServiceClient() {
|
|
44
|
+
if (_serviceClient)
|
|
45
|
+
return _serviceClient;
|
|
46
|
+
const url = envOrThrow('OPTIMAL_SUPABASE_URL');
|
|
47
|
+
const key = envOrThrow('OPTIMAL_SUPABASE_SERVICE_KEY');
|
|
48
|
+
_serviceClient = createClient(url, key, {
|
|
49
|
+
auth: {
|
|
50
|
+
persistSession: false,
|
|
51
|
+
autoRefreshToken: false,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return _serviceClient;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Return a user-scoped Supabase client authenticated with the given JWT.
|
|
58
|
+
*
|
|
59
|
+
* This is the CLI equivalent of optimalOS browser/server clients that carry
|
|
60
|
+
* a user session via cookies. The caller is responsible for obtaining the
|
|
61
|
+
* access token (e.g., via `supabase login`, OAuth device flow, or env var).
|
|
62
|
+
*
|
|
63
|
+
* A new client is created on every call — callers should cache if needed.
|
|
64
|
+
*/
|
|
65
|
+
export function getUserClient(accessToken) {
|
|
66
|
+
const url = envOrThrow('OPTIMAL_SUPABASE_URL');
|
|
67
|
+
// Use service key as the initial key — the global auth header override
|
|
68
|
+
// ensures all requests are scoped to the user's JWT instead.
|
|
69
|
+
const anonOrServiceKey = process.env.OPTIMAL_SUPABASE_ANON_KEY
|
|
70
|
+
?? envOrThrow('OPTIMAL_SUPABASE_SERVICE_KEY');
|
|
71
|
+
return createClient(url, anonOrServiceKey, {
|
|
72
|
+
global: {
|
|
73
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
74
|
+
},
|
|
75
|
+
auth: {
|
|
76
|
+
persistSession: false,
|
|
77
|
+
autoRefreshToken: false,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Attempt to retrieve the current session.
|
|
83
|
+
*
|
|
84
|
+
* In the CLI there is no implicit cookie jar. A session exists only when:
|
|
85
|
+
* 1. OPTIMAL_ACCESS_TOKEN env var is set (user JWT), or
|
|
86
|
+
* 2. A future `optimal login` command has cached a token locally.
|
|
87
|
+
*
|
|
88
|
+
* Returns null if no user session is available (service-role only mode).
|
|
89
|
+
*/
|
|
90
|
+
export async function getSession() {
|
|
91
|
+
const token = process.env.OPTIMAL_ACCESS_TOKEN;
|
|
92
|
+
if (!token)
|
|
93
|
+
return null;
|
|
94
|
+
try {
|
|
95
|
+
const client = getUserClient(token);
|
|
96
|
+
const { data: { user }, error } = await client.auth.getUser(token);
|
|
97
|
+
if (error || !user)
|
|
98
|
+
return null;
|
|
99
|
+
return {
|
|
100
|
+
accessToken: token,
|
|
101
|
+
user: {
|
|
102
|
+
id: user.id,
|
|
103
|
+
email: user.email,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Guard that throws if no user session is present.
|
|
113
|
+
*
|
|
114
|
+
* Use at the top of CLI commands that require a logged-in user:
|
|
115
|
+
*
|
|
116
|
+
* const session = await requireAuth()
|
|
117
|
+
* // session.user.id is guaranteed
|
|
118
|
+
*/
|
|
119
|
+
export async function requireAuth() {
|
|
120
|
+
const session = await getSession();
|
|
121
|
+
if (!session) {
|
|
122
|
+
throw new Error('Authentication required. Set OPTIMAL_ACCESS_TOKEN or run `optimal login`.');
|
|
123
|
+
}
|
|
124
|
+
return session;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build an AuthContext describing the current invocation's auth state.
|
|
128
|
+
*
|
|
129
|
+
* Prefers user-scoped auth when OPTIMAL_ACCESS_TOKEN is set;
|
|
130
|
+
* falls back to service-role.
|
|
131
|
+
*/
|
|
132
|
+
export async function resolveAuthContext() {
|
|
133
|
+
const session = await getSession();
|
|
134
|
+
if (session) {
|
|
135
|
+
return {
|
|
136
|
+
mode: 'user',
|
|
137
|
+
client: getUserClient(session.accessToken),
|
|
138
|
+
userId: session.user.id,
|
|
139
|
+
email: session.user.email,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
mode: 'service',
|
|
144
|
+
client: getServiceClient(),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Project, Task, Label, Comment, Milestone, ActivityEntry, CreateProjectInput, CreateTaskInput, CreateCommentInput, CreateMilestoneInput, UpdateTaskInput, TaskStatus } from './types.js';
|
|
2
|
+
export * from './types.js';
|
|
3
|
+
export declare function formatBoardTable(tasks: Task[]): string;
|
|
4
|
+
export declare function getNextClaimable(readyTasks: Task[], allTasks: Task[]): Task | null;
|
|
5
|
+
export declare function createProject(input: CreateProjectInput): Promise<Project>;
|
|
6
|
+
export declare function getProjectBySlug(slug: string): Promise<Project>;
|
|
7
|
+
export declare function listProjects(): Promise<Project[]>;
|
|
8
|
+
export declare function updateProject(slug: string, updates: Partial<Pick<Project, 'status' | 'owner' | 'priority' | 'description'>>): Promise<Project>;
|
|
9
|
+
export declare function createMilestone(input: CreateMilestoneInput): Promise<Milestone>;
|
|
10
|
+
export declare function listMilestones(projectId?: string): Promise<Milestone[]>;
|
|
11
|
+
export declare function createLabel(name: string, color?: string): Promise<Label>;
|
|
12
|
+
export declare function listLabels(): Promise<Label[]>;
|
|
13
|
+
export declare function getLabelByName(name: string): Promise<Label | null>;
|
|
14
|
+
export declare function createTask(input: CreateTaskInput): Promise<Task>;
|
|
15
|
+
export declare function updateTask(taskId: string, updates: UpdateTaskInput, actor?: string): Promise<Task>;
|
|
16
|
+
export declare function getTask(taskId: string): Promise<Task>;
|
|
17
|
+
export declare function listTasks(opts?: {
|
|
18
|
+
project_id?: string;
|
|
19
|
+
status?: TaskStatus;
|
|
20
|
+
claimed_by?: string;
|
|
21
|
+
assigned_to?: string;
|
|
22
|
+
}): Promise<Task[]>;
|
|
23
|
+
export declare function claimTask(taskId: string, agent: string): Promise<Task>;
|
|
24
|
+
export declare function completeTask(taskId: string, actor: string): Promise<Task>;
|
|
25
|
+
export declare function addComment(input: CreateCommentInput): Promise<Comment>;
|
|
26
|
+
export declare function listComments(taskId: string): Promise<Comment[]>;
|
|
27
|
+
export declare function logActivity(entry: {
|
|
28
|
+
task_id?: string;
|
|
29
|
+
project_id?: string;
|
|
30
|
+
actor: string;
|
|
31
|
+
action: string;
|
|
32
|
+
old_value?: Record<string, unknown>;
|
|
33
|
+
new_value?: Record<string, unknown>;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
export declare function listActivity(opts?: {
|
|
36
|
+
task_id?: string;
|
|
37
|
+
actor?: string;
|
|
38
|
+
limit?: number;
|
|
39
|
+
}): Promise<ActivityEntry[]>;
|