optimal-cli 1.0.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.
Files changed (135) hide show
  1. package/dist/bin/optimal.d.ts +2 -0
  2. package/dist/bin/optimal.js +1590 -0
  3. package/dist/lib/assets/index.d.ts +79 -0
  4. package/dist/lib/assets/index.js +153 -0
  5. package/dist/lib/assets.d.ts +20 -0
  6. package/dist/lib/assets.js +112 -0
  7. package/dist/lib/auth/index.d.ts +83 -0
  8. package/dist/lib/auth/index.js +146 -0
  9. package/dist/lib/board/index.d.ts +39 -0
  10. package/dist/lib/board/index.js +285 -0
  11. package/dist/lib/board/types.d.ts +111 -0
  12. package/dist/lib/board/types.js +1 -0
  13. package/dist/lib/bot/claim.d.ts +3 -0
  14. package/dist/lib/bot/claim.js +20 -0
  15. package/dist/lib/bot/coordinator.d.ts +27 -0
  16. package/dist/lib/bot/coordinator.js +178 -0
  17. package/dist/lib/bot/heartbeat.d.ts +6 -0
  18. package/dist/lib/bot/heartbeat.js +30 -0
  19. package/dist/lib/bot/index.d.ts +9 -0
  20. package/dist/lib/bot/index.js +6 -0
  21. package/dist/lib/bot/protocol.d.ts +12 -0
  22. package/dist/lib/bot/protocol.js +74 -0
  23. package/dist/lib/bot/reporter.d.ts +3 -0
  24. package/dist/lib/bot/reporter.js +27 -0
  25. package/dist/lib/bot/skills.d.ts +26 -0
  26. package/dist/lib/bot/skills.js +69 -0
  27. package/dist/lib/budget/projections.d.ts +115 -0
  28. package/dist/lib/budget/projections.js +384 -0
  29. package/dist/lib/budget/scenarios.d.ts +93 -0
  30. package/dist/lib/budget/scenarios.js +214 -0
  31. package/dist/lib/cms/publish-blog.d.ts +62 -0
  32. package/dist/lib/cms/publish-blog.js +74 -0
  33. package/dist/lib/cms/strapi-client.d.ts +123 -0
  34. package/dist/lib/cms/strapi-client.js +213 -0
  35. package/dist/lib/config/registry.d.ts +17 -0
  36. package/dist/lib/config/registry.js +182 -0
  37. package/dist/lib/config/schema.d.ts +31 -0
  38. package/dist/lib/config/schema.js +25 -0
  39. package/dist/lib/config.d.ts +55 -0
  40. package/dist/lib/config.js +206 -0
  41. package/dist/lib/errors.d.ts +25 -0
  42. package/dist/lib/errors.js +91 -0
  43. package/dist/lib/format.d.ts +28 -0
  44. package/dist/lib/format.js +98 -0
  45. package/dist/lib/infra/deploy.d.ts +29 -0
  46. package/dist/lib/infra/deploy.js +58 -0
  47. package/dist/lib/infra/migrate.d.ts +34 -0
  48. package/dist/lib/infra/migrate.js +103 -0
  49. package/dist/lib/newsletter/distribute.d.ts +52 -0
  50. package/dist/lib/newsletter/distribute.js +193 -0
  51. package/{lib/newsletter/generate-insurance.ts → dist/lib/newsletter/generate-insurance.d.ts} +7 -24
  52. package/dist/lib/newsletter/generate-insurance.js +36 -0
  53. package/dist/lib/newsletter/generate.d.ts +104 -0
  54. package/dist/lib/newsletter/generate.js +571 -0
  55. package/dist/lib/returnpro/anomalies.d.ts +64 -0
  56. package/dist/lib/returnpro/anomalies.js +166 -0
  57. package/dist/lib/returnpro/audit.d.ts +32 -0
  58. package/dist/lib/returnpro/audit.js +147 -0
  59. package/dist/lib/returnpro/diagnose.d.ts +52 -0
  60. package/dist/lib/returnpro/diagnose.js +281 -0
  61. package/dist/lib/returnpro/kpis.d.ts +32 -0
  62. package/dist/lib/returnpro/kpis.js +192 -0
  63. package/dist/lib/returnpro/templates.d.ts +48 -0
  64. package/dist/lib/returnpro/templates.js +229 -0
  65. package/dist/lib/returnpro/upload-income.d.ts +25 -0
  66. package/dist/lib/returnpro/upload-income.js +235 -0
  67. package/dist/lib/returnpro/upload-netsuite.d.ts +37 -0
  68. package/dist/lib/returnpro/upload-netsuite.js +566 -0
  69. package/dist/lib/returnpro/upload-r1.d.ts +48 -0
  70. package/dist/lib/returnpro/upload-r1.js +398 -0
  71. package/dist/lib/returnpro/validate.d.ts +37 -0
  72. package/dist/lib/returnpro/validate.js +124 -0
  73. package/dist/lib/social/meta.d.ts +90 -0
  74. package/dist/lib/social/meta.js +160 -0
  75. package/dist/lib/social/post-generator.d.ts +83 -0
  76. package/dist/lib/social/post-generator.js +333 -0
  77. package/dist/lib/social/publish.d.ts +66 -0
  78. package/dist/lib/social/publish.js +226 -0
  79. package/dist/lib/social/scraper.d.ts +67 -0
  80. package/dist/lib/social/scraper.js +361 -0
  81. package/dist/lib/supabase.d.ts +4 -0
  82. package/dist/lib/supabase.js +20 -0
  83. package/dist/lib/transactions/delete-batch.d.ts +60 -0
  84. package/dist/lib/transactions/delete-batch.js +203 -0
  85. package/dist/lib/transactions/ingest.d.ts +43 -0
  86. package/dist/lib/transactions/ingest.js +555 -0
  87. package/dist/lib/transactions/stamp.d.ts +51 -0
  88. package/dist/lib/transactions/stamp.js +524 -0
  89. package/package.json +3 -4
  90. package/bin/optimal.ts +0 -1731
  91. package/lib/assets/index.ts +0 -225
  92. package/lib/assets.ts +0 -124
  93. package/lib/auth/index.ts +0 -189
  94. package/lib/board/index.ts +0 -309
  95. package/lib/board/types.ts +0 -124
  96. package/lib/bot/claim.ts +0 -43
  97. package/lib/bot/coordinator.ts +0 -254
  98. package/lib/bot/heartbeat.ts +0 -37
  99. package/lib/bot/index.ts +0 -9
  100. package/lib/bot/protocol.ts +0 -99
  101. package/lib/bot/reporter.ts +0 -42
  102. package/lib/bot/skills.ts +0 -81
  103. package/lib/budget/projections.ts +0 -561
  104. package/lib/budget/scenarios.ts +0 -312
  105. package/lib/cms/publish-blog.ts +0 -129
  106. package/lib/cms/strapi-client.ts +0 -302
  107. package/lib/config/registry.ts +0 -228
  108. package/lib/config/schema.ts +0 -58
  109. package/lib/config.ts +0 -247
  110. package/lib/errors.ts +0 -129
  111. package/lib/format.ts +0 -120
  112. package/lib/infra/.gitkeep +0 -0
  113. package/lib/infra/deploy.ts +0 -70
  114. package/lib/infra/migrate.ts +0 -141
  115. package/lib/newsletter/.gitkeep +0 -0
  116. package/lib/newsletter/distribute.ts +0 -256
  117. package/lib/newsletter/generate.ts +0 -735
  118. package/lib/returnpro/.gitkeep +0 -0
  119. package/lib/returnpro/anomalies.ts +0 -258
  120. package/lib/returnpro/audit.ts +0 -194
  121. package/lib/returnpro/diagnose.ts +0 -400
  122. package/lib/returnpro/kpis.ts +0 -255
  123. package/lib/returnpro/templates.ts +0 -323
  124. package/lib/returnpro/upload-income.ts +0 -311
  125. package/lib/returnpro/upload-netsuite.ts +0 -696
  126. package/lib/returnpro/upload-r1.ts +0 -563
  127. package/lib/returnpro/validate.ts +0 -154
  128. package/lib/social/meta.ts +0 -228
  129. package/lib/social/post-generator.ts +0 -468
  130. package/lib/social/publish.ts +0 -301
  131. package/lib/social/scraper.ts +0 -503
  132. package/lib/supabase.ts +0 -25
  133. package/lib/transactions/delete-batch.ts +0 -258
  134. package/lib/transactions/ingest.ts +0 -659
  135. package/lib/transactions/stamp.ts +0 -654
@@ -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[]>;