@lifeaitools/clauth 2.1.1 → 2.10.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/cli/api.js +238 -238
- package/cli/commands/install.js +413 -396
- package/cli/commands/login.test.js +49 -1
- package/cli/commands/ops-install.js +11 -10
- package/cli/commands/ops.test.js +270 -0
- package/cli/commands/scrub.test.js +70 -0
- package/cli/commands/serve.js +2613 -11219
- package/cli/commands/uninstall.js +164 -164
- package/cli/dashboard/dashboard.js +592 -0
- package/cli/dashboard/global.css +65 -0
- package/cli/dashboard/panel-element.js +79 -0
- package/cli/dashboard/panels/build-status-panel.css +12 -0
- package/cli/dashboard/panels/build-status-panel.js +98 -0
- package/cli/dashboard/panels/panel-registry.js +58 -0
- package/cli/dashboard/panels/services-panel.css +95 -0
- package/cli/dashboard/panels/services-panel.js +872 -0
- package/cli/dashboard/panels/surfaces-panel.css +59 -0
- package/cli/dashboard/panels/surfaces-panel.js +474 -0
- package/cli/dashboard/panels/toolbar-panel.css +42 -0
- package/cli/dashboard/panels/toolbar-panel.js +374 -0
- package/cli/dashboard/panels/tunnel-panel.css +61 -0
- package/cli/dashboard/panels/tunnel-panel.js +688 -0
- package/cli/dashboard/panels/webdav-panel.css +38 -0
- package/cli/dashboard/panels/webdav-panel.js +166 -0
- package/cli/fingerprint.test.js +95 -0
- package/cli/http/components/callagent-terminal-component.js +310 -0
- package/cli/http/components/dashboard-component.js +123 -0
- package/cli/http/components/inbox-component.js +41 -0
- package/cli/http/components/mcp-transport-component.js +275 -0
- package/cli/http/components/oauth-component.js +207 -0
- package/cli/http/components/service-crud-component.js +288 -0
- package/cli/http/components/supervisor-component.js +104 -0
- package/cli/http/components/system-component.js +184 -0
- package/cli/http/components/tunnel-component.js +508 -0
- package/cli/http/components/vault-auth-component.js +264 -0
- package/cli/http/components/watchdog-component.js +28 -0
- package/cli/http/request-utils.js +21 -0
- package/cli/http/servers/ops-server.js +335 -0
- package/cli/index.js +5 -91
- package/cli/lib/fs-git.test.js +467 -0
- package/cli/mcp/providers/call-agent-provider.js +30 -0
- package/cli/mcp/providers/clauth-core-provider.js +461 -0
- package/cli/mcp/providers/ops-provider.js +42 -0
- package/cli/mcp/providers/terminal-provider.js +81 -0
- package/cli/mcp/tool-registry.js +48 -0
- package/cli/mcp/tool-schemas.js +822 -0
- package/cli/mcp/tool-schemas.test.js +49 -0
- package/cli/ops/pm2-preflight.js +54 -0
- package/cli/recovery.js +6 -0
- package/cli/recovery.test.js +179 -0
- package/cli/services/agent-pool-instance.js +38 -0
- package/cli/services/claude-binary.js +29 -0
- package/cli/services/delegation-lane-instance.js +35 -0
- package/cli/services/delegation-lane-instance.test.js +37 -0
- package/cli/services/file-io.js +38 -0
- package/cli/services/file-io.test.js +10 -0
- package/cli/services/fs-mount.js +66 -0
- package/cli/services/fs-mount.test.js +14 -0
- package/cli/services/fs-upload-sessions.js +20 -0
- package/cli/services/fs-upload-sessions.test.js +14 -0
- package/cli/services/git-exec.js +37 -0
- package/cli/services/git-exec.test.js +41 -0
- package/cli/services/git-import-guard.js +28 -0
- package/cli/services/git-import-guard.test.js +18 -0
- package/cli/services/service-status.js +191 -0
- package/cli/services/service-status.test.js +39 -0
- package/cli/services/terminal-dispatch.js +135 -0
- package/cli/services/terminal-dispatch.test.js +9 -0
- package/cli/services/terminal-registry.js +39 -0
- package/cli/services/terminal-registry.test.js +31 -0
- package/cli/services/terminal-sessions.js +100 -0
- package/cli/services/terminal-sessions.test.js +26 -0
- package/cli/services/terminal-window.js +106 -0
- package/cli/services/vault-read.js +14 -0
- package/cli/services/vault-read.test.js +35 -0
- package/cli/supervisor-registry.js +116 -0
- package/cli/supervisor-registry.test.js +165 -0
- package/cli/supervisor-ui.test.js +230 -30
- package/cli/webdav-config.js +84 -0
- package/cli/webdav-config.test.js +79 -0
- package/cli/webdav-obscure.js +23 -0
- package/cli/webdav-obscure.test.js +41 -0
- package/cli/webdav-rclone-discovery.js +44 -0
- package/cli/webdav-service.js +30 -118
- package/cli/webdav-service.test.js +107 -0
- package/install.ps1 +102 -102
- package/install.sh +49 -49
- package/package.json +2 -2
- package/scripts/bootstrap.cjs +121 -121
- 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
- package/cli/assets/codevelop/launcher-active.cmd.template +0 -20
- package/cli/assets/codevelop/launcher-static.cmd.template +0 -7
- package/cli/assets/codevelop/windows-terminal.profiles.json +0 -48
- package/cli/commands/codevelop.js +0 -1190
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// cli/webdav-rclone-discovery.js
|
|
2
|
+
// Locates a usable rclone binary across the common Windows install shapes
|
|
3
|
+
// (PATH, Program Files, scoop, winget's version-stamped package dir). Split
|
|
4
|
+
// out of webdav-service.js -- the SOLID scorer flagged that file's original
|
|
5
|
+
// shape as mixing this discovery concern with the rclone child supervisor,
|
|
6
|
+
// config persistence, and the rclone-obscure crypto helper.
|
|
7
|
+
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { readdirSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
export function findRclone() {
|
|
14
|
+
const home = homedir();
|
|
15
|
+
const wingetBase = process.env.LOCALAPPDATA
|
|
16
|
+
? join(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Packages")
|
|
17
|
+
: null;
|
|
18
|
+
const execOpts = { stdio: "pipe", timeout: 4000, windowsHide: true };
|
|
19
|
+
|
|
20
|
+
const candidates = [
|
|
21
|
+
"rclone",
|
|
22
|
+
"C:\\Program Files\\rclone\\rclone.exe",
|
|
23
|
+
join(home, "scoop", "apps", "rclone", "current", "rclone.exe"),
|
|
24
|
+
"C:\\ProgramData\\scoop\\apps\\rclone\\current\\rclone.exe",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Winget installs into a version-stamped subdirectory — glob for it
|
|
28
|
+
if (wingetBase) {
|
|
29
|
+
const wingetPkg = join(wingetBase, "Rclone.Rclone_Microsoft.Winget.Source_8wekyb3d8bbwe");
|
|
30
|
+
try {
|
|
31
|
+
for (const d of readdirSync(wingetPkg)) {
|
|
32
|
+
if (d.startsWith("rclone-")) candidates.push(join(wingetPkg, d, "rclone.exe"));
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const c of candidates) {
|
|
38
|
+
try {
|
|
39
|
+
execSync(`"${c}" version`, execOpts);
|
|
40
|
+
return c;
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
package/cli/webdav-service.js
CHANGED
|
@@ -8,78 +8,24 @@
|
|
|
8
8
|
// Design: lazy-start. The rclone child spawns on the first fs_dav_setup call.
|
|
9
9
|
// Subsequent calls return the running child's status without restarting.
|
|
10
10
|
// On child crash: exponential-backoff restart loop (2s → 5s → 15s → 60s).
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
//
|
|
12
|
+
// Config CRUD moved to webdav-config.js, the rclone-obscure crypto helper
|
|
13
|
+
// moved to webdav-obscure.js, and rclone binary discovery moved to
|
|
14
|
+
// webdav-rclone-discovery.js after the SOLID scorer flagged this file's
|
|
15
|
+
// original shape (74.8 total) as mixing all four concerns.
|
|
16
|
+
|
|
17
|
+
import { spawn } from "child_process";
|
|
18
|
+
import { writeFileSync, unlinkSync } from "fs";
|
|
19
|
+
import { tmpdir } from "os";
|
|
15
20
|
import { join } from "path";
|
|
16
|
-
import crypto from "crypto";
|
|
17
21
|
import * as api from "./api.js";
|
|
18
22
|
import { deriveToken } from "./fingerprint.js";
|
|
23
|
+
import { loadConfig, WEBDAV_PORT, CRED_NAME } from "./webdav-config.js";
|
|
24
|
+
import { findRclone } from "./webdav-rclone-discovery.js";
|
|
19
25
|
|
|
20
|
-
const WEBDAV_PORT = 8620;
|
|
21
26
|
const WEBDAV_USER = "claude";
|
|
22
|
-
const CRED_NAME = "webdav-claude";
|
|
23
27
|
const RESTART_DELAYS = [2000, 5000, 15000, 60000];
|
|
24
28
|
|
|
25
|
-
const APPDATA = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
|
|
26
|
-
const CONFIG_PATH = join(APPDATA, "clauth", "webdav-config.json");
|
|
27
|
-
|
|
28
|
-
const DEFAULT_CONFIG = {
|
|
29
|
-
enabled: true,
|
|
30
|
-
port: WEBDAV_PORT,
|
|
31
|
-
credential: CRED_NAME,
|
|
32
|
-
upstreams: [],
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// ── Config persistence ──────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
export function loadConfig() {
|
|
38
|
-
try {
|
|
39
|
-
const raw = readFileSync(CONFIG_PATH, "utf8");
|
|
40
|
-
const cfg = JSON.parse(raw);
|
|
41
|
-
if (!Array.isArray(cfg.upstreams)) cfg.upstreams = [];
|
|
42
|
-
return { ...DEFAULT_CONFIG, ...cfg };
|
|
43
|
-
} catch {
|
|
44
|
-
return { ...DEFAULT_CONFIG };
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function saveConfig(cfg) {
|
|
49
|
-
const dir = join(APPDATA, "clauth");
|
|
50
|
-
mkdirSync(dir, { recursive: true });
|
|
51
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), "utf8");
|
|
52
|
-
_fsCfgCache = null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function addMount(name, mountPath) {
|
|
56
|
-
const cfg = loadConfig();
|
|
57
|
-
if (cfg.upstreams.some(u => u.name === name)) return { error: `Mount '${name}' already exists` };
|
|
58
|
-
cfg.upstreams.push({ name, path: mountPath });
|
|
59
|
-
saveConfig(cfg);
|
|
60
|
-
return { ok: true, upstreams: cfg.upstreams };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function removeMount(name) {
|
|
64
|
-
const cfg = loadConfig();
|
|
65
|
-
const before = cfg.upstreams.length;
|
|
66
|
-
cfg.upstreams = cfg.upstreams.filter(u => u.name !== name);
|
|
67
|
-
if (cfg.upstreams.length === before) return { error: `Mount '${name}' not found` };
|
|
68
|
-
saveConfig(cfg);
|
|
69
|
-
return { ok: true, upstreams: cfg.upstreams };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function updateMount(name, updates) {
|
|
73
|
-
const cfg = loadConfig();
|
|
74
|
-
const mount = cfg.upstreams.find(u => u.name === name);
|
|
75
|
-
if (!mount) return { error: `Mount '${name}' not found` };
|
|
76
|
-
if (updates.credential !== undefined) cfg.credential = updates.credential;
|
|
77
|
-
saveConfig(cfg);
|
|
78
|
-
return { ok: true, upstreams: cfg.upstreams };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let _fsCfgCache = null;
|
|
82
|
-
|
|
83
29
|
// ── Module state ────────────────────────────────────────────────
|
|
84
30
|
let _child = null;
|
|
85
31
|
let _configPath = null;
|
|
@@ -131,25 +77,6 @@ export function shutdown() {
|
|
|
131
77
|
_status = "stopped";
|
|
132
78
|
}
|
|
133
79
|
|
|
134
|
-
/**
|
|
135
|
-
* rclone-compatible AES-CTR "obscure" — same algorithm as `rclone obscure`.
|
|
136
|
-
* Used by fs_dav_setup to embed a non-plaintext credential in the mount script.
|
|
137
|
-
* The result can be passed as RCLONE_CONFIG_xxx_PASS.
|
|
138
|
-
*/
|
|
139
|
-
export function rcloneObscure(password) {
|
|
140
|
-
// Key from rclone source: lib/obscure/obscure.go cryptKey
|
|
141
|
-
const key = Buffer.from([
|
|
142
|
-
0x9c, 0x93, 0x5b, 0x48, 0x73, 0x0a, 0x55, 0x4d,
|
|
143
|
-
0x6b, 0xfd, 0x7c, 0x63, 0xc8, 0x86, 0xd9, 0x2d,
|
|
144
|
-
0x02, 0x01, 0x99, 0x87, 0x54, 0xd3, 0x9b, 0xeb,
|
|
145
|
-
0x40, 0x87, 0x3f, 0xa1, 0x3f, 0x7a, 0xbe, 0x5e,
|
|
146
|
-
]);
|
|
147
|
-
const iv = crypto.randomBytes(16);
|
|
148
|
-
const cipher = crypto.createCipheriv("aes-256-ctr", key, iv);
|
|
149
|
-
const ct = Buffer.concat([cipher.update(Buffer.from(password, "utf8")), cipher.final()]);
|
|
150
|
-
return Buffer.concat([iv, ct]).toString("base64url");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
80
|
// ── Internal ─────────────────────────────────────────────────────
|
|
154
81
|
|
|
155
82
|
async function _startChild() {
|
|
@@ -187,7 +114,7 @@ async function _startChild() {
|
|
|
187
114
|
}
|
|
188
115
|
|
|
189
116
|
// 2. Locate rclone
|
|
190
|
-
const rclonePath =
|
|
117
|
+
const rclonePath = findRclone();
|
|
191
118
|
if (!rclonePath) {
|
|
192
119
|
_status = "error";
|
|
193
120
|
_error = "rclone not found — install with: winget install Rclone.Rclone";
|
|
@@ -298,42 +225,27 @@ function _scheduleRestart() {
|
|
|
298
225
|
}, delay);
|
|
299
226
|
}
|
|
300
227
|
|
|
228
|
+
// Test-only reset of the module-level supervisor state. The rclone child
|
|
229
|
+
// lifecycle is deliberately a module singleton (one WebDAV server per clauth
|
|
230
|
+
// process) rather than an instance, which is right for production and means a
|
|
231
|
+
// test file importing this module once needs a way to clear _status/_cfg/etc.
|
|
232
|
+
// between cases — otherwise the SECOND test in the file inherits whatever
|
|
233
|
+
// state the first one left behind.
|
|
234
|
+
export function __resetForTests() {
|
|
235
|
+
if (_child) { try { _child.removeAllListeners(); _child.kill("SIGKILL"); } catch {} }
|
|
236
|
+
_child = null;
|
|
237
|
+
_configPath = null;
|
|
238
|
+
_status = "stopped";
|
|
239
|
+
_error = null;
|
|
240
|
+
_restartCount = 0;
|
|
241
|
+
_startedAt = null;
|
|
242
|
+
_vaultFn = null;
|
|
243
|
+
_cfg = null;
|
|
244
|
+
}
|
|
245
|
+
|
|
301
246
|
function _cleanupConfig() {
|
|
302
247
|
if (_configPath) {
|
|
303
248
|
try { unlinkSync(_configPath); } catch {}
|
|
304
249
|
_configPath = null;
|
|
305
250
|
}
|
|
306
251
|
}
|
|
307
|
-
|
|
308
|
-
function _findRclone() {
|
|
309
|
-
const home = homedir();
|
|
310
|
-
const wingetBase = process.env.LOCALAPPDATA
|
|
311
|
-
? join(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Packages")
|
|
312
|
-
: null;
|
|
313
|
-
const execOpts = { stdio: "pipe", timeout: 4000, windowsHide: true };
|
|
314
|
-
|
|
315
|
-
const candidates = [
|
|
316
|
-
"rclone",
|
|
317
|
-
"C:\\Program Files\\rclone\\rclone.exe",
|
|
318
|
-
join(home, "scoop", "apps", "rclone", "current", "rclone.exe"),
|
|
319
|
-
"C:\\ProgramData\\scoop\\apps\\rclone\\current\\rclone.exe",
|
|
320
|
-
];
|
|
321
|
-
|
|
322
|
-
// Winget installs into a version-stamped subdirectory — glob for it
|
|
323
|
-
if (wingetBase) {
|
|
324
|
-
const wingetPkg = join(wingetBase, "Rclone.Rclone_Microsoft.Winget.Source_8wekyb3d8bbwe");
|
|
325
|
-
try {
|
|
326
|
-
for (const d of readdirSync(wingetPkg)) {
|
|
327
|
-
if (d.startsWith("rclone-")) candidates.push(join(wingetPkg, d, "rclone.exe"));
|
|
328
|
-
}
|
|
329
|
-
} catch {}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
for (const c of candidates) {
|
|
333
|
-
try {
|
|
334
|
-
execSync(`"${c}" version`, execOpts);
|
|
335
|
-
return c;
|
|
336
|
-
} catch {}
|
|
337
|
-
}
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { addMount, loadConfig } from "./webdav-config.js";
|
|
8
|
+
import { ensureRunning, getStatus, shutdown, __resetForTests } from "./webdav-service.js";
|
|
9
|
+
|
|
10
|
+
function withIsolatedConfig(fn) {
|
|
11
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-webdav-test-"));
|
|
12
|
+
const configPath = path.join(dir, "webdav-config.json");
|
|
13
|
+
const old = process.env.CLAUTH_WEBDAV_CONFIG_PATH;
|
|
14
|
+
process.env.CLAUTH_WEBDAV_CONFIG_PATH = configPath;
|
|
15
|
+
__resetForTests();
|
|
16
|
+
return (async () => {
|
|
17
|
+
try { return await fn(configPath); } finally {
|
|
18
|
+
__resetForTests();
|
|
19
|
+
if (old === undefined) Reflect.deleteProperty(process.env, "CLAUTH_WEBDAV_CONFIG_PATH");
|
|
20
|
+
else process.env.CLAUTH_WEBDAV_CONFIG_PATH = old;
|
|
21
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("getStatus with no config loaded reports stopped and no error", () =>
|
|
27
|
+
withIsolatedConfig(() => {
|
|
28
|
+
const status = getStatus();
|
|
29
|
+
assert.equal(status.status, "stopped");
|
|
30
|
+
assert.equal(status.url, null);
|
|
31
|
+
assert.equal(status.restart_count, 0);
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
test("ensureRunning refuses when no mount points are configured", () =>
|
|
35
|
+
withIsolatedConfig(async () => {
|
|
36
|
+
// loadConfig() called with no mounts added in THIS test's isolated dir.
|
|
37
|
+
const result = await ensureRunning(() => ({ password: "pw", machineHash: "h" }), loadConfig());
|
|
38
|
+
assert.match(result.error, /No mount points configured/);
|
|
39
|
+
assert.equal(getStatus().status, "error");
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
test("ensureRunning refuses when the vault is locked", () =>
|
|
43
|
+
withIsolatedConfig(async () => {
|
|
44
|
+
addMount("projects", "C:\\Dev\\regen-root");
|
|
45
|
+
const result = await ensureRunning(() => ({ password: null }), loadConfig());
|
|
46
|
+
assert.match(result.error, /Vault is locked/);
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
test("ensureRunning is idempotent — a second call while already running short-circuits without re-resolving the vault", () =>
|
|
50
|
+
withIsolatedConfig(async () => {
|
|
51
|
+
addMount("projects", "C:\\Dev\\regen-root");
|
|
52
|
+
let vaultCalls = 0;
|
|
53
|
+
const vaultFn = () => { vaultCalls++; return { password: null }; }; // locked, so _startChild always errors
|
|
54
|
+
await ensureRunning(vaultFn, loadConfig());
|
|
55
|
+
assert.equal(vaultCalls, 1);
|
|
56
|
+
// ensureRunning only short-circuits on status==="running"||"starting"; a
|
|
57
|
+
// locked vault leaves status "error", so a second call legitimately
|
|
58
|
+
// re-resolves — this asserts that behavior rather than a false "cached".
|
|
59
|
+
await ensureRunning(vaultFn, loadConfig());
|
|
60
|
+
assert.equal(vaultCalls, 2, "an errored (not running/starting) state must retry, not silently no-op forever");
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
test("ensureRunning proceeds past credential resolution once the vault yields a value", () =>
|
|
64
|
+
withIsolatedConfig(async () => {
|
|
65
|
+
// Deliberately does NOT let this reach _startChild's rclone spawn step.
|
|
66
|
+
// `findRclone()` was assumed absent on this box (`where rclone` exits 1),
|
|
67
|
+
// but that only checks PATH — this machine has rclone installed via winget
|
|
68
|
+
// at a version-stamped path findRclone()'s own glob fallback discovers
|
|
69
|
+
// (verified: C:/Users/.../AppData/Local/Microsoft/WinGet/Packages/
|
|
70
|
+
// Rclone.Rclone_.../rclone-v1.75.0-windows-amd64/rclone.exe exists). A test
|
|
71
|
+
// that reached the spawn step here would start a REAL rclone WebDAV server
|
|
72
|
+
// on port 8620 — a genuine side effect on a developer's real machine, not
|
|
73
|
+
// a safe thing for a unit test to risk regardless of which branch it hits.
|
|
74
|
+
//
|
|
75
|
+
// api.js's `retrieve` cannot be monkeypatched directly — ESM namespace
|
|
76
|
+
// exports are non-configurable, so `Object.defineProperty(api, "retrieve",
|
|
77
|
+
// ...)` throws "Cannot redefine property". It calls the global `fetch`
|
|
78
|
+
// underneath, which IS mockable, same technique as recovery.test.js.
|
|
79
|
+
addMount("projects", "C:\\Dev\\regen-root");
|
|
80
|
+
const oldFetch = globalThis.fetch;
|
|
81
|
+
const oldUrl = process.env.CLAUTH_SUPABASE_URL;
|
|
82
|
+
const oldKey = process.env.CLAUTH_SUPABASE_ANON_KEY;
|
|
83
|
+
process.env.CLAUTH_SUPABASE_URL = "https://example.supabase.co";
|
|
84
|
+
process.env.CLAUTH_SUPABASE_ANON_KEY = "anon-key-not-real";
|
|
85
|
+
let fetchCalled = false;
|
|
86
|
+
globalThis.fetch = async () => { fetchCalled = true; return { ok: true, status: 200, json: async () => ({ value: "" }) }; };
|
|
87
|
+
try {
|
|
88
|
+
// An EMPTY credential value hits `if (!result.value)` — the
|
|
89
|
+
// credential-not-found branch — which is a few lines before findRclone
|
|
90
|
+
// is ever called, so this can never reach the spawn step no matter what
|
|
91
|
+
// is or isn't installed on the box running it.
|
|
92
|
+
const result = await ensureRunning(() => ({ password: "pw", machineHash: "h" }), loadConfig());
|
|
93
|
+
assert.equal(fetchCalled, true, "control: the mock must actually have been invoked, or this proves nothing about the credential path");
|
|
94
|
+
assert.match(result.error, /not found — register it in the vault first/);
|
|
95
|
+
assert.equal(getStatus().status, "error");
|
|
96
|
+
} finally {
|
|
97
|
+
globalThis.fetch = oldFetch;
|
|
98
|
+
if (oldUrl === undefined) Reflect.deleteProperty(process.env, "CLAUTH_SUPABASE_URL"); else process.env.CLAUTH_SUPABASE_URL = oldUrl;
|
|
99
|
+
if (oldKey === undefined) Reflect.deleteProperty(process.env, "CLAUTH_SUPABASE_ANON_KEY"); else process.env.CLAUTH_SUPABASE_ANON_KEY = oldKey;
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
test("shutdown resets status to stopped even when nothing is running", () =>
|
|
104
|
+
withIsolatedConfig(() => {
|
|
105
|
+
shutdown();
|
|
106
|
+
assert.equal(getStatus().status, "stopped");
|
|
107
|
+
}));
|
package/install.ps1
CHANGED
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
# clauth installer - Windows
|
|
2
|
-
# Private repo - run locally:
|
|
3
|
-
# cd C:\Dev\clauth && git pull && .\install.ps1
|
|
4
|
-
|
|
5
|
-
$ErrorActionPreference = "Stop"
|
|
6
|
-
$REPO = "https://github.com/LIFEAI/clauth.git"
|
|
7
|
-
$DIR = "$env:USERPROFILE\.clauth"
|
|
8
|
-
|
|
9
|
-
# Check git
|
|
10
|
-
try { git --version | Out-Null } catch {
|
|
11
|
-
Write-Host ""
|
|
12
|
-
Write-Host " x git is required." -ForegroundColor Red
|
|
13
|
-
Write-Host " Install from https://git-scm.com then re-run this script."
|
|
14
|
-
Write-Host ""
|
|
15
|
-
exit 1
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
# Check Node
|
|
19
|
-
try { node --version | Out-Null } catch {
|
|
20
|
-
Write-Host ""
|
|
21
|
-
Write-Host " x Node.js v18+ is required." -ForegroundColor Red
|
|
22
|
-
Write-Host " Install from https://nodejs.org then re-run this script."
|
|
23
|
-
Write-Host ""
|
|
24
|
-
exit 1
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
# Check cloudflared (soft warning - not required for install)
|
|
28
|
-
if (-not (Get-Command cloudflared -ErrorAction SilentlyContinue)) {
|
|
29
|
-
Write-Host " Note: cloudflared not found. Install after setup for claude.ai web integration." -ForegroundColor Yellow
|
|
30
|
-
Write-Host " winget install Cloudflare.cloudflared" -ForegroundColor Gray
|
|
31
|
-
Write-Host ""
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
# Skip bootstrap if CLAUTH npm package is already installed globally
|
|
35
|
-
$alreadyInstalled = $false
|
|
36
|
-
try {
|
|
37
|
-
$ver = & npm list -g @lifeaitools/clauth --depth=0 2>$null | Select-String "@lifeaitools/clauth"
|
|
38
|
-
if ($ver) { $alreadyInstalled = $true }
|
|
39
|
-
} catch { }
|
|
40
|
-
|
|
41
|
-
if ($alreadyInstalled) {
|
|
42
|
-
Write-Host " clauth already installed globally - skipping bootstrap." -ForegroundColor Green
|
|
43
|
-
} else {
|
|
44
|
-
# Clone or update
|
|
45
|
-
if (Test-Path "$DIR\.git") {
|
|
46
|
-
Write-Host " Updating clauth..."
|
|
47
|
-
Set-Location $DIR; git pull --quiet
|
|
48
|
-
} else {
|
|
49
|
-
Write-Host " Cloning clauth..."
|
|
50
|
-
git clone --quiet $REPO $DIR
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
# Run compiled bootstrap binary
|
|
54
|
-
$bootstrap = "$DIR\scripts\bin\bootstrap-win.exe"
|
|
55
|
-
if (-not (Test-Path $bootstrap)) {
|
|
56
|
-
Write-Host " x Bootstrap binary not found at $bootstrap" -ForegroundColor Red
|
|
57
|
-
exit 1
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
Set-Location $DIR
|
|
61
|
-
& $bootstrap
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
# Register autostart - idempotent, runs every install/update
|
|
65
|
-
# Primary: Task Scheduler (preferred - supports delay and OS-managed restarts)
|
|
66
|
-
# Fallback: Startup folder shortcut (fully functional - autostart.ps1 has its own crash-recovery loop)
|
|
67
|
-
$autostartScript = "$env:APPDATA\clauth\autostart.ps1"
|
|
68
|
-
$taskName = "CLAUTH Daemon"
|
|
69
|
-
|
|
70
|
-
if (Test-Path $autostartScript) {
|
|
71
|
-
Write-Host ""
|
|
72
|
-
Write-Host " Registering autostart..." -ForegroundColor Cyan
|
|
73
|
-
|
|
74
|
-
$registered = $false
|
|
75
|
-
|
|
76
|
-
# Try Task Scheduler first
|
|
77
|
-
try {
|
|
78
|
-
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
|
79
|
-
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
|
|
80
|
-
-Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File `"$autostartScript`""
|
|
81
|
-
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
|
82
|
-
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit 0 -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
|
|
83
|
-
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
|
|
84
|
-
-Settings $settings -RunLevel Limited -Force -ErrorAction Stop | Out-Null
|
|
85
|
-
Write-Host " + Task Scheduler: '$taskName' registered (triggers at logon)." -ForegroundColor Green
|
|
86
|
-
$registered = $true
|
|
87
|
-
} catch { }
|
|
88
|
-
|
|
89
|
-
# Fallback: Startup folder - valid solution, autostart.ps1 handles crash recovery internally
|
|
90
|
-
if (-not $registered) {
|
|
91
|
-
$startupDir = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"
|
|
92
|
-
$shortcut = "$startupDir\clauth-autostart.lnk"
|
|
93
|
-
$wsh = New-Object -ComObject WScript.Shell
|
|
94
|
-
$lnk = $wsh.CreateShortcut($shortcut)
|
|
95
|
-
$lnk.TargetPath = "powershell.exe"
|
|
96
|
-
# 15s delay baked in so profile is loaded before daemon starts
|
|
97
|
-
$lnk.Arguments = "-WindowStyle Hidden -ExecutionPolicy Bypass -Command `"Start-Sleep 15; & '$autostartScript'`""
|
|
98
|
-
$lnk.WorkingDirectory = "$env:APPDATA\clauth"
|
|
99
|
-
$lnk.WindowStyle = 7
|
|
100
|
-
$lnk.Save()
|
|
101
|
-
Write-Host " + Autostart registered via Startup folder (15s delay, crash-recovery loop active)." -ForegroundColor Green
|
|
102
|
-
}
|
|
1
|
+
# clauth installer - Windows
|
|
2
|
+
# Private repo - run locally:
|
|
3
|
+
# cd C:\Dev\clauth && git pull && .\install.ps1
|
|
4
|
+
|
|
5
|
+
$ErrorActionPreference = "Stop"
|
|
6
|
+
$REPO = "https://github.com/LIFEAI/clauth.git"
|
|
7
|
+
$DIR = "$env:USERPROFILE\.clauth"
|
|
8
|
+
|
|
9
|
+
# Check git
|
|
10
|
+
try { git --version | Out-Null } catch {
|
|
11
|
+
Write-Host ""
|
|
12
|
+
Write-Host " x git is required." -ForegroundColor Red
|
|
13
|
+
Write-Host " Install from https://git-scm.com then re-run this script."
|
|
14
|
+
Write-Host ""
|
|
15
|
+
exit 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Check Node
|
|
19
|
+
try { node --version | Out-Null } catch {
|
|
20
|
+
Write-Host ""
|
|
21
|
+
Write-Host " x Node.js v18+ is required." -ForegroundColor Red
|
|
22
|
+
Write-Host " Install from https://nodejs.org then re-run this script."
|
|
23
|
+
Write-Host ""
|
|
24
|
+
exit 1
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Check cloudflared (soft warning - not required for install)
|
|
28
|
+
if (-not (Get-Command cloudflared -ErrorAction SilentlyContinue)) {
|
|
29
|
+
Write-Host " Note: cloudflared not found. Install after setup for claude.ai web integration." -ForegroundColor Yellow
|
|
30
|
+
Write-Host " winget install Cloudflare.cloudflared" -ForegroundColor Gray
|
|
31
|
+
Write-Host ""
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Skip bootstrap if CLAUTH npm package is already installed globally
|
|
35
|
+
$alreadyInstalled = $false
|
|
36
|
+
try {
|
|
37
|
+
$ver = & npm list -g @lifeaitools/clauth --depth=0 2>$null | Select-String "@lifeaitools/clauth"
|
|
38
|
+
if ($ver) { $alreadyInstalled = $true }
|
|
39
|
+
} catch { }
|
|
40
|
+
|
|
41
|
+
if ($alreadyInstalled) {
|
|
42
|
+
Write-Host " clauth already installed globally - skipping bootstrap." -ForegroundColor Green
|
|
43
|
+
} else {
|
|
44
|
+
# Clone or update
|
|
45
|
+
if (Test-Path "$DIR\.git") {
|
|
46
|
+
Write-Host " Updating clauth..."
|
|
47
|
+
Set-Location $DIR; git pull --quiet
|
|
48
|
+
} else {
|
|
49
|
+
Write-Host " Cloning clauth..."
|
|
50
|
+
git clone --quiet $REPO $DIR
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Run compiled bootstrap binary
|
|
54
|
+
$bootstrap = "$DIR\scripts\bin\bootstrap-win.exe"
|
|
55
|
+
if (-not (Test-Path $bootstrap)) {
|
|
56
|
+
Write-Host " x Bootstrap binary not found at $bootstrap" -ForegroundColor Red
|
|
57
|
+
exit 1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Set-Location $DIR
|
|
61
|
+
& $bootstrap
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Register autostart - idempotent, runs every install/update
|
|
65
|
+
# Primary: Task Scheduler (preferred - supports delay and OS-managed restarts)
|
|
66
|
+
# Fallback: Startup folder shortcut (fully functional - autostart.ps1 has its own crash-recovery loop)
|
|
67
|
+
$autostartScript = "$env:APPDATA\clauth\autostart.ps1"
|
|
68
|
+
$taskName = "CLAUTH Daemon"
|
|
69
|
+
|
|
70
|
+
if (Test-Path $autostartScript) {
|
|
71
|
+
Write-Host ""
|
|
72
|
+
Write-Host " Registering autostart..." -ForegroundColor Cyan
|
|
73
|
+
|
|
74
|
+
$registered = $false
|
|
75
|
+
|
|
76
|
+
# Try Task Scheduler first
|
|
77
|
+
try {
|
|
78
|
+
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
|
79
|
+
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
|
|
80
|
+
-Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File `"$autostartScript`""
|
|
81
|
+
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
|
82
|
+
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit 0 -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
|
|
83
|
+
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
|
|
84
|
+
-Settings $settings -RunLevel Limited -Force -ErrorAction Stop | Out-Null
|
|
85
|
+
Write-Host " + Task Scheduler: '$taskName' registered (triggers at logon)." -ForegroundColor Green
|
|
86
|
+
$registered = $true
|
|
87
|
+
} catch { }
|
|
88
|
+
|
|
89
|
+
# Fallback: Startup folder - valid solution, autostart.ps1 handles crash recovery internally
|
|
90
|
+
if (-not $registered) {
|
|
91
|
+
$startupDir = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"
|
|
92
|
+
$shortcut = "$startupDir\clauth-autostart.lnk"
|
|
93
|
+
$wsh = New-Object -ComObject WScript.Shell
|
|
94
|
+
$lnk = $wsh.CreateShortcut($shortcut)
|
|
95
|
+
$lnk.TargetPath = "powershell.exe"
|
|
96
|
+
# 15s delay baked in so profile is loaded before daemon starts
|
|
97
|
+
$lnk.Arguments = "-WindowStyle Hidden -ExecutionPolicy Bypass -Command `"Start-Sleep 15; & '$autostartScript'`""
|
|
98
|
+
$lnk.WorkingDirectory = "$env:APPDATA\clauth"
|
|
99
|
+
$lnk.WindowStyle = 7
|
|
100
|
+
$lnk.Save()
|
|
101
|
+
Write-Host " + Autostart registered via Startup folder (15s delay, crash-recovery loop active)." -ForegroundColor Green
|
|
102
|
+
}
|
|
103
103
|
} else {
|
|
104
104
|
Write-Host ""
|
|
105
105
|
Write-Host " ! autostart.ps1 not found - skipping autostart registration." -ForegroundColor Yellow
|
package/install.sh
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# clauth installer
|
|
3
|
-
set -e
|
|
4
|
-
|
|
5
|
-
REPO="https://github.com/LIFEAI/clauth.git"
|
|
6
|
-
DIR="$HOME/.clauth"
|
|
7
|
-
|
|
8
|
-
# Check git
|
|
9
|
-
if ! command -v git &>/dev/null; then
|
|
10
|
-
echo ""; echo " x git is required."
|
|
11
|
-
echo " Install git from https://git-scm.com then re-run this script."; echo ""
|
|
12
|
-
exit 1
|
|
13
|
-
fi
|
|
14
|
-
|
|
15
|
-
# Check Node
|
|
16
|
-
if ! command -v node &>/dev/null; then
|
|
17
|
-
echo ""; echo " x Node.js v18+ is required."
|
|
18
|
-
echo " Install from https://nodejs.org then re-run this script."; echo ""
|
|
19
|
-
exit 1
|
|
20
|
-
fi
|
|
21
|
-
|
|
22
|
-
# Check cloudflared (soft warning)
|
|
23
|
-
if ! command -v cloudflared &>/dev/null; then
|
|
24
|
-
echo " Note: cloudflared not found. Install after setup for claude.ai web integration."
|
|
25
|
-
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
26
|
-
echo " brew install cloudflare/cloudflare/cloudflared"
|
|
27
|
-
else
|
|
28
|
-
echo " https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"
|
|
29
|
-
fi
|
|
30
|
-
echo ""
|
|
31
|
-
fi
|
|
32
|
-
|
|
33
|
-
# Clone or update
|
|
34
|
-
if [ -d "$DIR/.git" ]; then
|
|
35
|
-
echo " Updating clauth..."; cd "$DIR" && git pull --quiet
|
|
36
|
-
else
|
|
37
|
-
echo " Cloning clauth..."; git clone --quiet "$REPO" "$DIR"
|
|
38
|
-
fi
|
|
39
|
-
|
|
40
|
-
# Run compiled bootstrap
|
|
41
|
-
UNAME=$(uname -s | tr '[:upper:]' '[:lower:]')
|
|
42
|
-
if [[ "$UNAME" == *"darwin"* ]]; then
|
|
43
|
-
BOOTSTRAP="$DIR/scripts/bin/bootstrap-macos"
|
|
44
|
-
else
|
|
45
|
-
BOOTSTRAP="$DIR/scripts/bin/bootstrap-linux"
|
|
46
|
-
fi
|
|
47
|
-
|
|
48
|
-
chmod +x "$BOOTSTRAP"
|
|
49
|
-
cd "$DIR" && "$BOOTSTRAP"
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# clauth installer
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
REPO="https://github.com/LIFEAI/clauth.git"
|
|
6
|
+
DIR="$HOME/.clauth"
|
|
7
|
+
|
|
8
|
+
# Check git
|
|
9
|
+
if ! command -v git &>/dev/null; then
|
|
10
|
+
echo ""; echo " x git is required."
|
|
11
|
+
echo " Install git from https://git-scm.com then re-run this script."; echo ""
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Check Node
|
|
16
|
+
if ! command -v node &>/dev/null; then
|
|
17
|
+
echo ""; echo " x Node.js v18+ is required."
|
|
18
|
+
echo " Install from https://nodejs.org then re-run this script."; echo ""
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Check cloudflared (soft warning)
|
|
23
|
+
if ! command -v cloudflared &>/dev/null; then
|
|
24
|
+
echo " Note: cloudflared not found. Install after setup for claude.ai web integration."
|
|
25
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
26
|
+
echo " brew install cloudflare/cloudflare/cloudflared"
|
|
27
|
+
else
|
|
28
|
+
echo " https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"
|
|
29
|
+
fi
|
|
30
|
+
echo ""
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Clone or update
|
|
34
|
+
if [ -d "$DIR/.git" ]; then
|
|
35
|
+
echo " Updating clauth..."; cd "$DIR" && git pull --quiet
|
|
36
|
+
else
|
|
37
|
+
echo " Cloning clauth..."; git clone --quiet "$REPO" "$DIR"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Run compiled bootstrap
|
|
41
|
+
UNAME=$(uname -s | tr '[:upper:]' '[:lower:]')
|
|
42
|
+
if [[ "$UNAME" == *"darwin"* ]]; then
|
|
43
|
+
BOOTSTRAP="$DIR/scripts/bin/bootstrap-macos"
|
|
44
|
+
else
|
|
45
|
+
BOOTSTRAP="$DIR/scripts/bin/bootstrap-linux"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
chmod +x "$BOOTSTRAP"
|
|
49
|
+
cd "$DIR" && "$BOOTSTRAP"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifeaitools/clauth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "node scripts/build.mjs",
|
|
11
|
-
"test": "node --test cli/commands/scrub.test.js cli/commands/login.test.js cli/api.classify.test.js cli/supervisor-registry.test.js cli/supervisor-ui.test.js test/studio-debug.test.mjs test/dashboard-key-generator.test.mjs && node test-auth-verdict.mjs",
|
|
11
|
+
"test": "node --test cli/commands/scrub.test.js cli/commands/login.test.js cli/api.classify.test.js cli/supervisor-registry.test.js cli/supervisor-ui.test.js cli/fingerprint.test.js cli/recovery.test.js cli/watchdog-registry.test.js cli/commands/ops.test.js cli/lib/fs-git.test.js cli/webdav-service.test.js cli/webdav-config.test.js cli/webdav-obscure.test.js cli/mcp/tool-schemas.test.js cli/services/service-status.test.js cli/services/fs-mount.test.js cli/services/fs-upload-sessions.test.js cli/services/git-exec.test.js cli/services/file-io.test.js cli/services/vault-read.test.js cli/services/git-import-guard.test.js cli/services/delegation-lane-instance.test.js cli/services/terminal-sessions.test.js cli/services/terminal-registry.test.js cli/services/terminal-dispatch.test.js test/studio-debug.test.mjs test/dashboard-key-generator.test.mjs test/agent-corpus.test.mjs test/agent-cron.test.mjs test/agent-delegation.test.mjs test/agent-pool.test.mjs test/coolify-adapter.test.mjs test/coolify-promote-uuid.test.mjs test/deployment-adapter.test.mjs test/enrollment-script.test.mjs test/ops-client.test.mjs test/ops-fixtures.test.mjs test/ops-install.test.mjs test/ops-job-store.test.mjs test/ops-pm2-e2e.test.mjs test/pm2-adapter.test.mjs test/serialized-executor.test.mjs test/call-agent-guard.test.mjs test/serve-http-routes.test.mjs && node test-auth-verdict.mjs",
|
|
12
12
|
"test:studio-debug": "node --test test/studio-debug.test.mjs",
|
|
13
13
|
"test:agent-pool": "node test/agent-pool.test.mjs",
|
|
14
14
|
"test:call-agent-10": "node test/call-agent-10-skills.test.mjs",
|