@lifeaitools/clauth 1.30.26 → 2.0.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/.clauth-skill/references/keys-guide.md +270 -270
- package/README.md +2 -0
- package/cli/api.js +238 -238
- package/cli/commands/agent-pool.js +51 -15
- package/cli/commands/install.js +396 -396
- package/cli/commands/login.js +135 -0
- package/cli/commands/login.test.js +73 -0
- package/cli/commands/serve.js +290 -1555
- package/cli/commands/uninstall.js +164 -164
- package/cli/index.js +102 -1
- package/cli/supervisor-registry.js +106 -5
- package/cli/supervisor-registry.test.js +151 -0
- package/cli/webdav-service.js +339 -339
- package/install.ps1 +102 -102
- package/install.sh +49 -49
- package/package.json +4 -5
- package/scripts/bootstrap.cjs +121 -121
- package/scripts/build.mjs +66 -0
- package/scripts/build.sh +5 -45
- package/scripts/postinstall.js +189 -189
- package/supabase/functions/auth-vault/index.ts +350 -350
- package/supabase/migrations/001_clauth_schema.sql +94 -94
- package/supabase/migrations/002_vault_helpers.sql +90 -90
- package/supabase/migrations/20260317_lockout.sql +26 -26
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
// cli/commands/uninstall.js
|
|
2
|
-
// clauth uninstall — full teardown: DB objects, Edge Function, secrets, skill, local config
|
|
3
|
-
//
|
|
4
|
-
// Reverses everything `clauth install` does:
|
|
5
|
-
// 1. Drops clauth tables, policies, triggers, functions from Supabase
|
|
6
|
-
// 2. Deletes auth-vault Edge Function
|
|
7
|
-
// 3. Removes CLAUTH_* secrets
|
|
8
|
-
// 4. Removes Claude skill directory
|
|
9
|
-
// 5. Clears local config (Conf store)
|
|
10
|
-
|
|
11
|
-
import { existsSync, rmSync } from 'fs';
|
|
12
|
-
import { join } from 'path';
|
|
13
|
-
import Conf from 'conf';
|
|
14
|
-
import chalk from 'chalk';
|
|
15
|
-
import ora from 'ora';
|
|
16
|
-
|
|
17
|
-
const MGMT = 'https://api.supabase.com/v1';
|
|
18
|
-
const SKILLS_DIR = process.env.CLAUTH_SKILLS_DIR ||
|
|
19
|
-
(process.platform === 'win32'
|
|
20
|
-
? join(process.env.USERPROFILE || '', '.claude', 'skills')
|
|
21
|
-
: join(process.env.HOME || '', '.claude', 'skills'));
|
|
22
|
-
|
|
23
|
-
// ─────────────────────────────────────────────
|
|
24
|
-
// Supabase Management API helper
|
|
25
|
-
// ─────────────────────────────────────────────
|
|
26
|
-
async function mgmt(pat, method, path, body) {
|
|
27
|
-
const res = await fetch(`${MGMT}${path}`, {
|
|
28
|
-
method,
|
|
29
|
-
headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
|
|
30
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
31
|
-
});
|
|
32
|
-
if (!res.ok) {
|
|
33
|
-
const text = await res.text().catch(() => res.statusText);
|
|
34
|
-
throw new Error(`${method} ${path} → HTTP ${res.status}: ${text}`);
|
|
35
|
-
}
|
|
36
|
-
if (res.status === 204) return {};
|
|
37
|
-
const text = await res.text();
|
|
38
|
-
if (!text) return {};
|
|
39
|
-
return JSON.parse(text);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ─────────────────────────────────────────────
|
|
43
|
-
// Main uninstall command
|
|
44
|
-
// ─────────────────────────────────────────────
|
|
45
|
-
export async function runUninstall(opts = {}) {
|
|
46
|
-
console.log(chalk.red('\n🗑️ clauth uninstall\n'));
|
|
47
|
-
|
|
48
|
-
const config = new Conf({ projectName: 'clauth' });
|
|
49
|
-
|
|
50
|
-
// ── Collect credentials ────────────────────
|
|
51
|
-
const ref = opts.ref || config.get('supabase_url')?.match(/https:\/\/(.+)\.supabase\.co/)?.[1];
|
|
52
|
-
const pat = opts.pat;
|
|
53
|
-
|
|
54
|
-
if (!ref) {
|
|
55
|
-
console.log(chalk.red(' Cannot determine Supabase project ref.'));
|
|
56
|
-
console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
59
|
-
if (!pat) {
|
|
60
|
-
console.log(chalk.red(' Supabase PAT required for teardown.'));
|
|
61
|
-
console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
|
|
62
|
-
process.exit(1);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
console.log(chalk.gray(` Project: ${ref}\n`));
|
|
66
|
-
|
|
67
|
-
// ── Step 1: Drop database objects ──────────
|
|
68
|
-
const s1 = ora('Dropping clauth database objects...').start();
|
|
69
|
-
const teardownSQL = `
|
|
70
|
-
-- Drop triggers
|
|
71
|
-
DROP TRIGGER IF EXISTS clauth_services_updated ON public.clauth_services;
|
|
72
|
-
|
|
73
|
-
-- Drop tables (CASCADE drops policies automatically)
|
|
74
|
-
DROP TABLE IF EXISTS public.clauth_audit CASCADE;
|
|
75
|
-
DROP TABLE IF EXISTS public.clauth_machines CASCADE;
|
|
76
|
-
DROP TABLE IF EXISTS public.clauth_services CASCADE;
|
|
77
|
-
|
|
78
|
-
-- Drop functions
|
|
79
|
-
DROP FUNCTION IF EXISTS public.clauth_touch_updated() CASCADE;
|
|
80
|
-
DROP FUNCTION IF EXISTS public.clauth_upsert_vault_secret(text, text) CASCADE;
|
|
81
|
-
DROP FUNCTION IF EXISTS public.clauth_get_vault_secret(text) CASCADE;
|
|
82
|
-
DROP FUNCTION IF EXISTS public.clauth_delete_vault_secret(text) CASCADE;
|
|
83
|
-
`;
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: teardownSQL });
|
|
87
|
-
s1.succeed('Database objects dropped (tables, triggers, functions, policies)');
|
|
88
|
-
} catch (e) {
|
|
89
|
-
s1.fail(`Database teardown failed: ${e.message}`);
|
|
90
|
-
console.log(chalk.yellow(' You may need to drop objects manually via SQL editor.'));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── Step 2: Delete Edge Function ───────────
|
|
94
|
-
const s2 = ora('Deleting auth-vault Edge Function...').start();
|
|
95
|
-
try {
|
|
96
|
-
const res = await fetch(`${MGMT}/projects/${ref}/functions/auth-vault`, {
|
|
97
|
-
method: 'DELETE',
|
|
98
|
-
headers: { 'Authorization': `Bearer ${pat}` },
|
|
99
|
-
});
|
|
100
|
-
if (res.ok || res.status === 404) {
|
|
101
|
-
s2.succeed(res.status === 404
|
|
102
|
-
? 'Edge Function not found (already deleted)'
|
|
103
|
-
: 'Edge Function deleted');
|
|
104
|
-
} else {
|
|
105
|
-
const text = await res.text().catch(() => res.statusText);
|
|
106
|
-
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
107
|
-
}
|
|
108
|
-
} catch (e) {
|
|
109
|
-
s2.fail(`Edge Function delete failed: ${e.message}`);
|
|
110
|
-
console.log(chalk.yellow(' Delete manually: Supabase Dashboard → Edge Functions → auth-vault → Delete'));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ── Step 3: Remove secrets ─────────────────
|
|
114
|
-
const s3 = ora('Removing clauth secrets...').start();
|
|
115
|
-
try {
|
|
116
|
-
// Supabase Management API: DELETE /projects/{ref}/secrets with body listing secret names
|
|
117
|
-
const res = await fetch(`${MGMT}/projects/${ref}/secrets`, {
|
|
118
|
-
method: 'DELETE',
|
|
119
|
-
headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
|
|
120
|
-
body: JSON.stringify(['CLAUTH_HMAC_SALT', 'CLAUTH_ADMIN_BOOTSTRAP_TOKEN']),
|
|
121
|
-
});
|
|
122
|
-
if (res.ok) {
|
|
123
|
-
s3.succeed('Secrets removed (CLAUTH_HMAC_SALT, CLAUTH_ADMIN_BOOTSTRAP_TOKEN)');
|
|
124
|
-
} else {
|
|
125
|
-
const text = await res.text().catch(() => res.statusText);
|
|
126
|
-
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
127
|
-
}
|
|
128
|
-
} catch (e) {
|
|
129
|
-
s3.warn(`Secret removal failed: ${e.message}`);
|
|
130
|
-
console.log(chalk.yellow(' Remove manually: Supabase → Settings → Edge Functions → Secrets'));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ── Step 4: Remove Claude skill ────────────
|
|
134
|
-
const s4 = ora('Removing Claude skill...').start();
|
|
135
|
-
const skillDir = join(SKILLS_DIR, 'clauth');
|
|
136
|
-
if (existsSync(skillDir)) {
|
|
137
|
-
try {
|
|
138
|
-
rmSync(skillDir, { recursive: true, force: true });
|
|
139
|
-
s4.succeed(`Skill removed: ${skillDir}`);
|
|
140
|
-
} catch (e) {
|
|
141
|
-
s4.warn(`Could not remove skill: ${e.message}`);
|
|
142
|
-
}
|
|
143
|
-
} else {
|
|
144
|
-
s4.succeed('Skill directory not found (already removed)');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ── Step 5: Clear local config ─────────────
|
|
148
|
-
const s5 = ora('Clearing local config...').start();
|
|
149
|
-
try {
|
|
150
|
-
config.clear();
|
|
151
|
-
s5.succeed('Local config cleared');
|
|
152
|
-
} catch (e) {
|
|
153
|
-
s5.warn(`Could not clear config: ${e.message}`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ── Done ───────────────────────────────────
|
|
157
|
-
console.log('');
|
|
158
|
-
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
159
|
-
console.log(chalk.yellow(' ✓ clauth fully uninstalled'));
|
|
160
|
-
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
161
|
-
console.log('');
|
|
162
|
-
console.log(chalk.gray(' To reinstall: npx @lifeaitools/clauth install'));
|
|
163
|
-
console.log('');
|
|
164
|
-
}
|
|
1
|
+
// cli/commands/uninstall.js
|
|
2
|
+
// clauth uninstall — full teardown: DB objects, Edge Function, secrets, skill, local config
|
|
3
|
+
//
|
|
4
|
+
// Reverses everything `clauth install` does:
|
|
5
|
+
// 1. Drops clauth tables, policies, triggers, functions from Supabase
|
|
6
|
+
// 2. Deletes auth-vault Edge Function
|
|
7
|
+
// 3. Removes CLAUTH_* secrets
|
|
8
|
+
// 4. Removes Claude skill directory
|
|
9
|
+
// 5. Clears local config (Conf store)
|
|
10
|
+
|
|
11
|
+
import { existsSync, rmSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import Conf from 'conf';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import ora from 'ora';
|
|
16
|
+
|
|
17
|
+
const MGMT = 'https://api.supabase.com/v1';
|
|
18
|
+
const SKILLS_DIR = process.env.CLAUTH_SKILLS_DIR ||
|
|
19
|
+
(process.platform === 'win32'
|
|
20
|
+
? join(process.env.USERPROFILE || '', '.claude', 'skills')
|
|
21
|
+
: join(process.env.HOME || '', '.claude', 'skills'));
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────
|
|
24
|
+
// Supabase Management API helper
|
|
25
|
+
// ─────────────────────────────────────────────
|
|
26
|
+
async function mgmt(pat, method, path, body) {
|
|
27
|
+
const res = await fetch(`${MGMT}${path}`, {
|
|
28
|
+
method,
|
|
29
|
+
headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
|
|
30
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const text = await res.text().catch(() => res.statusText);
|
|
34
|
+
throw new Error(`${method} ${path} → HTTP ${res.status}: ${text}`);
|
|
35
|
+
}
|
|
36
|
+
if (res.status === 204) return {};
|
|
37
|
+
const text = await res.text();
|
|
38
|
+
if (!text) return {};
|
|
39
|
+
return JSON.parse(text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────
|
|
43
|
+
// Main uninstall command
|
|
44
|
+
// ─────────────────────────────────────────────
|
|
45
|
+
export async function runUninstall(opts = {}) {
|
|
46
|
+
console.log(chalk.red('\n🗑️ clauth uninstall\n'));
|
|
47
|
+
|
|
48
|
+
const config = new Conf({ projectName: 'clauth' });
|
|
49
|
+
|
|
50
|
+
// ── Collect credentials ────────────────────
|
|
51
|
+
const ref = opts.ref || config.get('supabase_url')?.match(/https:\/\/(.+)\.supabase\.co/)?.[1];
|
|
52
|
+
const pat = opts.pat;
|
|
53
|
+
|
|
54
|
+
if (!ref) {
|
|
55
|
+
console.log(chalk.red(' Cannot determine Supabase project ref.'));
|
|
56
|
+
console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
if (!pat) {
|
|
60
|
+
console.log(chalk.red(' Supabase PAT required for teardown.'));
|
|
61
|
+
console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.gray(` Project: ${ref}\n`));
|
|
66
|
+
|
|
67
|
+
// ── Step 1: Drop database objects ──────────
|
|
68
|
+
const s1 = ora('Dropping clauth database objects...').start();
|
|
69
|
+
const teardownSQL = `
|
|
70
|
+
-- Drop triggers
|
|
71
|
+
DROP TRIGGER IF EXISTS clauth_services_updated ON public.clauth_services;
|
|
72
|
+
|
|
73
|
+
-- Drop tables (CASCADE drops policies automatically)
|
|
74
|
+
DROP TABLE IF EXISTS public.clauth_audit CASCADE;
|
|
75
|
+
DROP TABLE IF EXISTS public.clauth_machines CASCADE;
|
|
76
|
+
DROP TABLE IF EXISTS public.clauth_services CASCADE;
|
|
77
|
+
|
|
78
|
+
-- Drop functions
|
|
79
|
+
DROP FUNCTION IF EXISTS public.clauth_touch_updated() CASCADE;
|
|
80
|
+
DROP FUNCTION IF EXISTS public.clauth_upsert_vault_secret(text, text) CASCADE;
|
|
81
|
+
DROP FUNCTION IF EXISTS public.clauth_get_vault_secret(text) CASCADE;
|
|
82
|
+
DROP FUNCTION IF EXISTS public.clauth_delete_vault_secret(text) CASCADE;
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: teardownSQL });
|
|
87
|
+
s1.succeed('Database objects dropped (tables, triggers, functions, policies)');
|
|
88
|
+
} catch (e) {
|
|
89
|
+
s1.fail(`Database teardown failed: ${e.message}`);
|
|
90
|
+
console.log(chalk.yellow(' You may need to drop objects manually via SQL editor.'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Step 2: Delete Edge Function ───────────
|
|
94
|
+
const s2 = ora('Deleting auth-vault Edge Function...').start();
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(`${MGMT}/projects/${ref}/functions/auth-vault`, {
|
|
97
|
+
method: 'DELETE',
|
|
98
|
+
headers: { 'Authorization': `Bearer ${pat}` },
|
|
99
|
+
});
|
|
100
|
+
if (res.ok || res.status === 404) {
|
|
101
|
+
s2.succeed(res.status === 404
|
|
102
|
+
? 'Edge Function not found (already deleted)'
|
|
103
|
+
: 'Edge Function deleted');
|
|
104
|
+
} else {
|
|
105
|
+
const text = await res.text().catch(() => res.statusText);
|
|
106
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
s2.fail(`Edge Function delete failed: ${e.message}`);
|
|
110
|
+
console.log(chalk.yellow(' Delete manually: Supabase Dashboard → Edge Functions → auth-vault → Delete'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Step 3: Remove secrets ─────────────────
|
|
114
|
+
const s3 = ora('Removing clauth secrets...').start();
|
|
115
|
+
try {
|
|
116
|
+
// Supabase Management API: DELETE /projects/{ref}/secrets with body listing secret names
|
|
117
|
+
const res = await fetch(`${MGMT}/projects/${ref}/secrets`, {
|
|
118
|
+
method: 'DELETE',
|
|
119
|
+
headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify(['CLAUTH_HMAC_SALT', 'CLAUTH_ADMIN_BOOTSTRAP_TOKEN']),
|
|
121
|
+
});
|
|
122
|
+
if (res.ok) {
|
|
123
|
+
s3.succeed('Secrets removed (CLAUTH_HMAC_SALT, CLAUTH_ADMIN_BOOTSTRAP_TOKEN)');
|
|
124
|
+
} else {
|
|
125
|
+
const text = await res.text().catch(() => res.statusText);
|
|
126
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
s3.warn(`Secret removal failed: ${e.message}`);
|
|
130
|
+
console.log(chalk.yellow(' Remove manually: Supabase → Settings → Edge Functions → Secrets'));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Step 4: Remove Claude skill ────────────
|
|
134
|
+
const s4 = ora('Removing Claude skill...').start();
|
|
135
|
+
const skillDir = join(SKILLS_DIR, 'clauth');
|
|
136
|
+
if (existsSync(skillDir)) {
|
|
137
|
+
try {
|
|
138
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
139
|
+
s4.succeed(`Skill removed: ${skillDir}`);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
s4.warn(`Could not remove skill: ${e.message}`);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
s4.succeed('Skill directory not found (already removed)');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Step 5: Clear local config ─────────────
|
|
148
|
+
const s5 = ora('Clearing local config...').start();
|
|
149
|
+
try {
|
|
150
|
+
config.clear();
|
|
151
|
+
s5.succeed('Local config cleared');
|
|
152
|
+
} catch (e) {
|
|
153
|
+
s5.warn(`Could not clear config: ${e.message}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Done ───────────────────────────────────
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
159
|
+
console.log(chalk.yellow(' ✓ clauth fully uninstalled'));
|
|
160
|
+
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(chalk.gray(' To reinstall: npx @lifeaitools/clauth install'));
|
|
163
|
+
console.log('');
|
|
164
|
+
}
|
package/cli/index.js
CHANGED
|
@@ -149,11 +149,41 @@ program
|
|
|
149
149
|
import { runInstall } from './commands/install.js';
|
|
150
150
|
import { runUninstall } from './commands/uninstall.js';
|
|
151
151
|
import { runScrub } from './commands/scrub.js';
|
|
152
|
-
import { runServe } from './commands/serve.js';
|
|
152
|
+
import { runServe, MCP_TOOLS } from './commands/serve.js';
|
|
153
|
+
import { listPlugins, registerPlugin } from './supervisor-registry.js';
|
|
153
154
|
import { runOps } from './commands/ops.js';
|
|
154
155
|
import { runOpsInstall } from './commands/ops-install.js';
|
|
155
156
|
import { runCodevelop } from './commands/codevelop.js';
|
|
156
157
|
import { runNpm, runPublish } from './commands/npm.js';
|
|
158
|
+
import { runLogin } from './commands/login.js';
|
|
159
|
+
|
|
160
|
+
// ──────────────────────────────────────────────
|
|
161
|
+
// clauth login <target>
|
|
162
|
+
// THE global login directive — the one sanctioned way to SSH into any
|
|
163
|
+
// clauth-managed box. Never hand-fetch a private key (clauth get <ssh-service>)
|
|
164
|
+
// and hand-roll ssh-agent/ssh yourself; add the box to LOGIN_TARGETS in
|
|
165
|
+
// cli/commands/login.js instead and use this command.
|
|
166
|
+
// ──────────────────────────────────────────────
|
|
167
|
+
program
|
|
168
|
+
.command('login [target]')
|
|
169
|
+
.description('SSH into a clauth-managed box by name (e.g. vultr) — run with no target to list them')
|
|
170
|
+
.option('-c, --command <cmd>', 'Run one command instead of opening an interactive shell')
|
|
171
|
+
.option('--host <user@host>', 'Override the target\'s default host')
|
|
172
|
+
.option('--native-windows', 'Human-only: use native Windows OpenSSH instead of Git Bash (blocked for agent sessions)')
|
|
173
|
+
.addHelpText('after', `
|
|
174
|
+
Examples:
|
|
175
|
+
clauth login List known login targets
|
|
176
|
+
clauth login vultr Interactive shell on the Vultr dev box
|
|
177
|
+
clauth login vultr -c "pm2 list" Run one command, non-interactive
|
|
178
|
+
clauth login vultr --host root@1.2.3.4 Override the default host for this target
|
|
179
|
+
`)
|
|
180
|
+
.action(async (target, opts) => {
|
|
181
|
+
await runLogin(target, {
|
|
182
|
+
command: opts.command,
|
|
183
|
+
host: opts.host,
|
|
184
|
+
nativeWindows: opts.nativeWindows,
|
|
185
|
+
});
|
|
186
|
+
});
|
|
157
187
|
|
|
158
188
|
program
|
|
159
189
|
.command('install')
|
|
@@ -933,6 +963,77 @@ tunnelCmd
|
|
|
933
963
|
}
|
|
934
964
|
});
|
|
935
965
|
|
|
966
|
+
// ──────────────────────────────────────────────
|
|
967
|
+
// clauth mcp list
|
|
968
|
+
// ──────────────────────────────────────────────
|
|
969
|
+
// Known MCP-server plugin ids in the managed fleet. Positive allowlist
|
|
970
|
+
// rather than a naming heuristic (credential-name conventions and health
|
|
971
|
+
// route "kind" vary across these) — dev-center and any future non-MCP
|
|
972
|
+
// pm2-managed surface stay excluded by construction. Deliberately distinct
|
|
973
|
+
// from `clauth list` (vault credential services); this never touches those.
|
|
974
|
+
const MCP_SERVER_PLUGIN_IDS = new Set([
|
|
975
|
+
"fs-mcp",
|
|
976
|
+
"web-research",
|
|
977
|
+
"regen-media",
|
|
978
|
+
"regen-media-local",
|
|
979
|
+
"codeflow-mcp",
|
|
980
|
+
"rdc-skills",
|
|
981
|
+
]);
|
|
982
|
+
|
|
983
|
+
const mcpCmd = program.command("mcp").description("Inspect clauth's own MCP tool catalog and managed MCP-server surfaces");
|
|
984
|
+
|
|
985
|
+
mcpCmd
|
|
986
|
+
.command("list")
|
|
987
|
+
.description("List clauth's advertised MCP tools and MCP-server surfaces (never vault credential services)")
|
|
988
|
+
.action(() => {
|
|
989
|
+
console.log(chalk.cyan(`\n clauth's own MCP tools (${MCP_TOOLS.length}):\n`));
|
|
990
|
+
for (const tool of MCP_TOOLS) {
|
|
991
|
+
console.log(` ${chalk.white(tool.name)} ${chalk.gray(tool.description || "")}`);
|
|
992
|
+
}
|
|
993
|
+
const mcpPlugins = listPlugins().filter((p) => MCP_SERVER_PLUGIN_IDS.has(p.id));
|
|
994
|
+
console.log(chalk.cyan(`\n Managed MCP-server surfaces (${mcpPlugins.length}):\n`));
|
|
995
|
+
if (!mcpPlugins.length) {
|
|
996
|
+
console.log(chalk.gray(" none discovered — is the daemon running? clauth serve"));
|
|
997
|
+
}
|
|
998
|
+
for (const plugin of mcpPlugins) {
|
|
999
|
+
const state = plugin.enabled ? plugin.state : "disabled";
|
|
1000
|
+
const icon = state === "current" ? "✓" : state === "disabled" ? "○" : "⚠";
|
|
1001
|
+
console.log(` ${icon} ${chalk.white(plugin.id)} ${chalk.gray(state)}`);
|
|
1002
|
+
for (const surface of plugin.surfaces || []) {
|
|
1003
|
+
console.log(` ${surface.id} ${chalk.gray(surface.health || surface.routes?.[0]?.url || "no health route")}`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
console.log("");
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
// ──────────────────────────────────────────────
|
|
1010
|
+
// clauth plugin register <manifest-path>
|
|
1011
|
+
// The self-registration entry point for PLUGIN-ARCHITECTURE-DECISION.md:
|
|
1012
|
+
// a product repo's own install/deploy step calls this after its
|
|
1013
|
+
// clauth-plugin.json is in place — same role as clauth's own
|
|
1014
|
+
// scripts/postinstall.js, for repos that have no npm-install lifecycle
|
|
1015
|
+
// hook of their own (monorepo workspace members, not standalone packages).
|
|
1016
|
+
// ──────────────────────────────────────────────
|
|
1017
|
+
const pluginCmd = program.command('plugin').description("Register and manage clauth-supervised plugin manifests");
|
|
1018
|
+
|
|
1019
|
+
pluginCmd
|
|
1020
|
+
.command('register <manifestPath>')
|
|
1021
|
+
.description('Validate a clauth-plugin.json and register it with the local supervisor, then run discovery')
|
|
1022
|
+
.action((manifestPath) => {
|
|
1023
|
+
const resolved = path.resolve(process.cwd(), manifestPath);
|
|
1024
|
+
const receipt = registerPlugin(resolved, 'cli');
|
|
1025
|
+
const ok = receipt.resulting_state?.ok;
|
|
1026
|
+
const state = receipt.resulting_state?.state;
|
|
1027
|
+
if (!ok) {
|
|
1028
|
+
const pluginState = receipt.resulting_state?.plugin_state;
|
|
1029
|
+
const detail = receipt.resulting_state?.error || (pluginState ? `plugin_state=${pluginState}` : 'registration failed');
|
|
1030
|
+
console.error(` ✗ ${state}: ${detail}`);
|
|
1031
|
+
process.exitCode = 1;
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
console.log(` ✓ ${chalk.white(receipt.target?.plugin_id || '?')} ${state} — surfaces: ${(receipt.resulting_state?.surfaces || []).join(', ') || 'none'}`);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
936
1037
|
// ──────────────────────────────────────────────
|
|
937
1038
|
// clauth chitchat --session <id>
|
|
938
1039
|
// ──────────────────────────────────────────────
|
|
@@ -167,7 +167,26 @@ function normalizeCommand(command, field) {
|
|
|
167
167
|
const [cmd, ...args] = command;
|
|
168
168
|
if (typeof cmd !== "string" || !cmd.trim()) throw new Error(`${field}[0] is required`);
|
|
169
169
|
if (/[;&|<>]/.test(cmd)) throw new Error(`${field}[0] must be an executable path/name, not shell syntax`);
|
|
170
|
-
|
|
170
|
+
const normalizedArgs = args.map(String);
|
|
171
|
+
// shell:true (Windows-only, see execute() in runSurfaceAction) hands each
|
|
172
|
+
// arg to cmd.exe verbatim — a shell metacharacter in an arg is exactly as
|
|
173
|
+
// exploitable as one in cmd[0]. A legitimate CLI arg for the pm2/node
|
|
174
|
+
// invocations this schema targets never needs raw shell syntax.
|
|
175
|
+
for (const [i, arg] of normalizedArgs.entries()) {
|
|
176
|
+
if (/[;&|<>]/.test(arg)) throw new Error(`${field}[${i + 1}] must not contain shell syntax`);
|
|
177
|
+
}
|
|
178
|
+
return [cmd, ...normalizedArgs];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Quotes a value for cmd.exe /c when shell:true is active and the value
|
|
182
|
+
// contains whitespace — spawnSync does NOT auto-quote in that mode, so an
|
|
183
|
+
// absolute path like "C:\Program Files\nodejs\node.exe" (or any arg with a
|
|
184
|
+
// space) breaks at the first space unless the caller quotes it. Idempotent:
|
|
185
|
+
// an already-quoted value is left as-is rather than double-quoted.
|
|
186
|
+
export function shellQuote(value, useShell) {
|
|
187
|
+
if (!useShell || !/\s/.test(value)) return value;
|
|
188
|
+
if (value.startsWith('"') && value.endsWith('"')) return value;
|
|
189
|
+
return `"${value}"`;
|
|
171
190
|
}
|
|
172
191
|
|
|
173
192
|
function expandPathToken(value) {
|
|
@@ -229,7 +248,7 @@ function localhostHealth(pathOrUrl, port) {
|
|
|
229
248
|
function normalizeSurface(surface, plugin) {
|
|
230
249
|
if (!surface || typeof surface !== "object") throw new Error("surface must be an object");
|
|
231
250
|
const id = String(surface.id || "").trim();
|
|
232
|
-
if (!/^[a-zA-Z0-9_.-]+$/.test(id)) throw new Error("surface.id may contain only letters, numbers, dot, underscore, and dash");
|
|
251
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(id) || /^\.+$/.test(id)) throw new Error("surface.id may contain only letters, numbers, dot, underscore, and dash, and may not be all dots");
|
|
233
252
|
const destination = normalizeDestination(surface.destination || plugin.destination);
|
|
234
253
|
const lifecycle_owner = normalizeLifecycleOwner(surface.lifecycle_owner || plugin.lifecycle_owner);
|
|
235
254
|
const port = surface.port === undefined || surface.port === null || surface.port === "auto" ? surface.port ?? null : Number(surface.port);
|
|
@@ -254,7 +273,7 @@ export function validatePluginManifest(manifest, sourcePath = "") {
|
|
|
254
273
|
if (!manifest || typeof manifest !== "object") throw new Error("manifest must be an object");
|
|
255
274
|
if (manifest.schema !== SCHEMA) throw new Error(`schema must be ${SCHEMA}`);
|
|
256
275
|
const id = String(manifest.id || "").trim();
|
|
257
|
-
if (!/^[a-zA-Z0-9_.-]+$/.test(id)) throw new Error("id may contain only letters, numbers, dot, underscore, and dash");
|
|
276
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(id) || /^\.+$/.test(id)) throw new Error("id may contain only letters, numbers, dot, underscore, and dash, and may not be all dots");
|
|
258
277
|
const version = String(manifest.version || "").trim();
|
|
259
278
|
if (!version) throw new Error("version is required");
|
|
260
279
|
const plugin = {
|
|
@@ -306,6 +325,17 @@ function findManifestFiles(root) {
|
|
|
306
325
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
307
326
|
const full = path.join(root, entry.name);
|
|
308
327
|
if (entry.isDirectory()) {
|
|
328
|
+
// A scoped npm package (@lifeaitools/fs-mcp) installs two levels deep
|
|
329
|
+
// under node_modules/@scope/pkg-name/ — descend one extra level only
|
|
330
|
+
// for @scope directories. Unscoped layout (one level) is unchanged.
|
|
331
|
+
if (entry.name.startsWith("@")) {
|
|
332
|
+
for (const scopedEntry of fs.readdirSync(full, { withFileTypes: true })) {
|
|
333
|
+
if (!scopedEntry.isDirectory()) continue;
|
|
334
|
+
const scopedCandidate = path.join(full, scopedEntry.name, "clauth-plugin.json");
|
|
335
|
+
if (fs.existsSync(scopedCandidate)) out.push(scopedCandidate);
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
309
339
|
const candidate = path.join(full, "clauth-plugin.json");
|
|
310
340
|
if (fs.existsSync(candidate)) out.push(candidate);
|
|
311
341
|
} else if (entry.isFile() && entry.name === "clauth-plugin.json") {
|
|
@@ -394,6 +424,65 @@ export function discoverPlugins() {
|
|
|
394
424
|
return { plugins, surfaces, events };
|
|
395
425
|
}
|
|
396
426
|
|
|
427
|
+
// Registers one plugin manifest into the managed-plugins root, then runs
|
|
428
|
+
// discovery so it's picked up immediately. This is the entry point a product
|
|
429
|
+
// repo's own install/deploy step calls to self-register — the mechanism the
|
|
430
|
+
// PLUGIN-ARCHITECTURE-DECISION.md "each MCP ships its own clauth-plugin.json"
|
|
431
|
+
// model needs for a monorepo workspace member (no npm install lifecycle hook
|
|
432
|
+
// to piggyback on, unlike a standalone published package with postinstall.js).
|
|
433
|
+
// Idempotent: re-registering unchanged content is a safe no-op re-affirm.
|
|
434
|
+
export function registerPlugin(manifestPath, actor = "localhost") {
|
|
435
|
+
let raw;
|
|
436
|
+
try {
|
|
437
|
+
raw = fs.readFileSync(manifestPath, "utf8");
|
|
438
|
+
} catch (error) {
|
|
439
|
+
return operation("plugin.register", { manifest_path: manifestPath }, null, {
|
|
440
|
+
ok: false, state: "manifest_unreadable", error: error instanceof Error ? error.message : String(error),
|
|
441
|
+
}, actor);
|
|
442
|
+
}
|
|
443
|
+
let manifest;
|
|
444
|
+
try {
|
|
445
|
+
manifest = validatePluginManifest(JSON.parse(raw), manifestPath);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
return operation("plugin.register", { manifest_path: manifestPath }, null, {
|
|
448
|
+
ok: false, state: "manifest_invalid", error: error instanceof Error ? error.message : String(error),
|
|
449
|
+
}, actor);
|
|
450
|
+
}
|
|
451
|
+
const [{ root: managedRoot }] = rootEntries();
|
|
452
|
+
const resolvedRoot = path.resolve(managedRoot);
|
|
453
|
+
const targetDir = path.resolve(managedRoot, manifest.id);
|
|
454
|
+
// Belt-and-braces: the id regex already rejects traversal-shaped ids, but
|
|
455
|
+
// this asserts containment at the actual write site so a future regex
|
|
456
|
+
// relaxation can't silently reopen a path escape out of the managed root —
|
|
457
|
+
// this is a credential vault writing files from parsed manifest content.
|
|
458
|
+
if (targetDir !== resolvedRoot && !targetDir.startsWith(resolvedRoot + path.sep)) {
|
|
459
|
+
return operation("plugin.register", { manifest_path: manifestPath, plugin_id: manifest.id }, null, {
|
|
460
|
+
ok: false, state: "manifest_invalid", error: "plugin id escapes the managed plugin root",
|
|
461
|
+
}, actor);
|
|
462
|
+
}
|
|
463
|
+
const targetPath = path.join(targetDir, "clauth-plugin.json");
|
|
464
|
+
const priorRaw = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : null;
|
|
465
|
+
const unchanged = priorRaw !== null && sha256(priorRaw) === sha256(raw);
|
|
466
|
+
if (!unchanged) {
|
|
467
|
+
try {
|
|
468
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
469
|
+
fs.writeFileSync(targetPath, raw, "utf8");
|
|
470
|
+
} catch (error) {
|
|
471
|
+
return operation("plugin.register", { manifest_path: manifestPath, plugin_id: manifest.id }, null, {
|
|
472
|
+
ok: false, state: "write_failed", error: error instanceof Error ? error.message : String(error),
|
|
473
|
+
}, actor);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const discovery = discoverPlugins();
|
|
477
|
+
const registered = discovery.plugins.find((plugin) => plugin.id === manifest.id);
|
|
478
|
+
return operation("plugin.register", { manifest_path: manifestPath, plugin_id: manifest.id }, null, {
|
|
479
|
+
ok: Boolean(registered) && registered.state !== "manifest_invalid",
|
|
480
|
+
state: unchanged ? "unchanged" : "registered",
|
|
481
|
+
plugin_state: registered?.state || "not_found",
|
|
482
|
+
surfaces: registered?.surfaces?.map((surface) => surface.id) || [],
|
|
483
|
+
}, actor);
|
|
484
|
+
}
|
|
485
|
+
|
|
397
486
|
export function listPlugins() {
|
|
398
487
|
return loadSupervisorState().plugins || [];
|
|
399
488
|
}
|
|
@@ -422,7 +511,7 @@ export function readSupervisorEvents(limit = 100) {
|
|
|
422
511
|
});
|
|
423
512
|
}
|
|
424
513
|
|
|
425
|
-
function operation(action, target, prior, result, actor = "localhost") {
|
|
514
|
+
export function operation(action, target, prior, result, actor = "localhost") {
|
|
426
515
|
const receipt = {
|
|
427
516
|
operationId: crypto.randomUUID(),
|
|
428
517
|
actor,
|
|
@@ -522,10 +611,22 @@ export function runSurfaceAction(id, action, actor = "localhost") {
|
|
|
522
611
|
}
|
|
523
612
|
const execute = (selectedCommand) => {
|
|
524
613
|
const [cmd, ...args] = selectedCommand;
|
|
525
|
-
|
|
614
|
+
const useShell = process.platform === "win32";
|
|
615
|
+
// With shell:true on Windows, spawnSync hands cmd/args to cmd.exe /c
|
|
616
|
+
// verbatim and does NOT auto-quote — an absolute path or arg containing
|
|
617
|
+
// a space (e.g. "C:\Program Files\nodejs\node.exe") breaks at the first
|
|
618
|
+
// space unless quoted. Bare shim names (pm2, npm) never contain spaces,
|
|
619
|
+
// so this only ever affects absolute-path values, and only on Windows.
|
|
620
|
+
const resolvedCmd = shellQuote(cmd, useShell);
|
|
621
|
+
const resolvedArgs = args.map((arg) => shellQuote(arg, useShell));
|
|
622
|
+
return spawnSync(resolvedCmd, resolvedArgs, {
|
|
526
623
|
cwd: surface.cwd || undefined,
|
|
527
624
|
env: { ...process.env, CLAUTH_PM2_HOME: getClauthPm2Home(), PM2_HOME: getClauthPm2Home() },
|
|
528
625
|
windowsHide: true,
|
|
626
|
+
// Windows resolves CLI shims (pm2, npm, etc.) to .cmd files that
|
|
627
|
+
// spawnSync cannot exec directly without a shell — ENOENT otherwise.
|
|
628
|
+
// POSIX targets (Vultr/Coolify) need no shell and keep prior behavior.
|
|
629
|
+
shell: useShell,
|
|
529
630
|
encoding: "utf8",
|
|
530
631
|
timeout: Number(surface.timeoutMs || 30000),
|
|
531
632
|
});
|