@n1creator/openacp-cli 2026.713.1 → 2026.713.2
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/README.md +3 -3
- package/dist/cli.js +2706 -1061
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +83 -5
- package/dist/index.js +1230 -446
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -458,22 +458,22 @@ __export(config_registry_exports, {
|
|
|
458
458
|
resolveOptions: () => resolveOptions,
|
|
459
459
|
setFieldValueAsync: () => setFieldValueAsync
|
|
460
460
|
});
|
|
461
|
-
function getFieldDef(
|
|
462
|
-
return CONFIG_REGISTRY.find((f) => f.path ===
|
|
461
|
+
function getFieldDef(path42) {
|
|
462
|
+
return CONFIG_REGISTRY.find((f) => f.path === path42);
|
|
463
463
|
}
|
|
464
464
|
function getSafeFields() {
|
|
465
465
|
return CONFIG_REGISTRY.filter((f) => f.scope === "safe");
|
|
466
466
|
}
|
|
467
|
-
function isHotReloadable(
|
|
468
|
-
const def = getFieldDef(
|
|
467
|
+
function isHotReloadable(path42) {
|
|
468
|
+
const def = getFieldDef(path42);
|
|
469
469
|
return def?.hotReload ?? false;
|
|
470
470
|
}
|
|
471
471
|
function resolveOptions(def, config) {
|
|
472
472
|
if (!def.options) return void 0;
|
|
473
473
|
return typeof def.options === "function" ? def.options(config) : def.options;
|
|
474
474
|
}
|
|
475
|
-
function getConfigValue(config,
|
|
476
|
-
const parts =
|
|
475
|
+
function getConfigValue(config, path42) {
|
|
476
|
+
const parts = path42.split(".");
|
|
477
477
|
let current = config;
|
|
478
478
|
for (const part of parts) {
|
|
479
479
|
if (current && typeof current === "object" && part in current) {
|
|
@@ -1624,27 +1624,27 @@ function validateExtractedPaths(destDir) {
|
|
|
1624
1624
|
}
|
|
1625
1625
|
}
|
|
1626
1626
|
async function extractTarGz(buffer, destDir) {
|
|
1627
|
-
const { execFileSync:
|
|
1627
|
+
const { execFileSync: execFileSync9 } = await import("child_process");
|
|
1628
1628
|
const tmpFile = path10.join(destDir, "_archive.tar.gz");
|
|
1629
1629
|
fs11.writeFileSync(tmpFile, buffer);
|
|
1630
1630
|
try {
|
|
1631
|
-
const listing =
|
|
1631
|
+
const listing = execFileSync9("tar", ["tf", tmpFile], { stdio: "pipe" }).toString().trim().split("\n").filter(Boolean);
|
|
1632
1632
|
validateArchiveContents(listing, destDir);
|
|
1633
|
-
|
|
1633
|
+
execFileSync9("tar", ["xzf", tmpFile, "-C", destDir], { stdio: "pipe" });
|
|
1634
1634
|
} finally {
|
|
1635
1635
|
fs11.unlinkSync(tmpFile);
|
|
1636
1636
|
}
|
|
1637
1637
|
validateExtractedPaths(destDir);
|
|
1638
1638
|
}
|
|
1639
1639
|
async function extractZip(buffer, destDir) {
|
|
1640
|
-
const { execFileSync:
|
|
1640
|
+
const { execFileSync: execFileSync9 } = await import("child_process");
|
|
1641
1641
|
const tmpFile = path10.join(destDir, "_archive.zip");
|
|
1642
1642
|
fs11.writeFileSync(tmpFile, buffer);
|
|
1643
1643
|
try {
|
|
1644
|
-
const listing =
|
|
1644
|
+
const listing = execFileSync9("unzip", ["-l", tmpFile], { stdio: "pipe" }).toString().trim().split("\n").filter(Boolean);
|
|
1645
1645
|
const entries = listing.slice(3, -2).map((line) => line.trim().split(/\s+/).slice(3).join(" ")).filter(Boolean);
|
|
1646
1646
|
validateArchiveContents(entries, destDir);
|
|
1647
|
-
|
|
1647
|
+
execFileSync9("unzip", ["-o", tmpFile, "-d", destDir], { stdio: "pipe" });
|
|
1648
1648
|
} finally {
|
|
1649
1649
|
fs11.unlinkSync(tmpFile);
|
|
1650
1650
|
}
|
|
@@ -2684,25 +2684,50 @@ function mimeToExt(mimeType) {
|
|
|
2684
2684
|
};
|
|
2685
2685
|
return map[mimeType] || ".bin";
|
|
2686
2686
|
}
|
|
2687
|
-
var GROQ_API_URL, GroqSTT;
|
|
2687
|
+
var GROQ_API_URL, GROQ_MODELS_URL, GroqSTT;
|
|
2688
2688
|
var init_groq = __esm({
|
|
2689
2689
|
"src/plugins/speech/providers/groq.ts"() {
|
|
2690
2690
|
"use strict";
|
|
2691
2691
|
GROQ_API_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
|
|
2692
|
+
GROQ_MODELS_URL = "https://api.groq.com/openai/v1/models";
|
|
2692
2693
|
GroqSTT = class {
|
|
2693
|
-
constructor(apiKey, defaultModel = "whisper-large-v3-turbo", scopedFetch = globalThis.fetch, getScopedFetch, endpoint = GROQ_API_URL) {
|
|
2694
|
+
constructor(apiKey, defaultModel = "whisper-large-v3-turbo", scopedFetch = globalThis.fetch, getScopedFetch, endpoint = GROQ_API_URL, accessEndpoint = GROQ_MODELS_URL) {
|
|
2694
2695
|
this.apiKey = apiKey;
|
|
2695
2696
|
this.defaultModel = defaultModel;
|
|
2696
2697
|
this.scopedFetch = scopedFetch;
|
|
2697
2698
|
this.getScopedFetch = getScopedFetch;
|
|
2698
2699
|
this.endpoint = endpoint;
|
|
2700
|
+
this.accessEndpoint = accessEndpoint;
|
|
2699
2701
|
}
|
|
2700
2702
|
apiKey;
|
|
2701
2703
|
defaultModel;
|
|
2702
2704
|
scopedFetch;
|
|
2703
2705
|
getScopedFetch;
|
|
2704
2706
|
endpoint;
|
|
2707
|
+
accessEndpoint;
|
|
2705
2708
|
name = "groq";
|
|
2709
|
+
/** Validate credentials without sending audio or exposing a provider response body. */
|
|
2710
|
+
async checkAccess(signal) {
|
|
2711
|
+
let response;
|
|
2712
|
+
try {
|
|
2713
|
+
response = await (this.getScopedFetch?.() ?? this.scopedFetch)(this.accessEndpoint, {
|
|
2714
|
+
method: "GET",
|
|
2715
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
2716
|
+
signal
|
|
2717
|
+
});
|
|
2718
|
+
} catch {
|
|
2719
|
+
return { ok: false, message: "Could not reach Groq through the configured speech route." };
|
|
2720
|
+
}
|
|
2721
|
+
try {
|
|
2722
|
+
await response.body?.cancel();
|
|
2723
|
+
} catch {
|
|
2724
|
+
}
|
|
2725
|
+
if (response.ok) return { ok: true, status: response.status, message: "Groq accepted the API key." };
|
|
2726
|
+
if (response.status === 401) return { ok: false, status: 401, message: "Groq rejected the API key." };
|
|
2727
|
+
if (response.status === 403) return { ok: false, status: 403, message: "The API key does not have access to Groq transcription." };
|
|
2728
|
+
if (response.status === 429) return { ok: false, status: 429, message: "Groq temporarily rate-limited the access check. Try again later." };
|
|
2729
|
+
return { ok: false, status: response.status, message: `Groq access check failed (HTTP ${response.status}).` };
|
|
2730
|
+
}
|
|
2706
2731
|
/**
|
|
2707
2732
|
* Transcribes audio using the Groq Whisper API.
|
|
2708
2733
|
*
|
|
@@ -2724,7 +2749,10 @@ var init_groq = __esm({
|
|
|
2724
2749
|
body: form
|
|
2725
2750
|
});
|
|
2726
2751
|
if (!resp.ok) {
|
|
2727
|
-
|
|
2752
|
+
try {
|
|
2753
|
+
await resp.body?.cancel();
|
|
2754
|
+
} catch {
|
|
2755
|
+
}
|
|
2728
2756
|
if (resp.status === 401) {
|
|
2729
2757
|
throw new Error("Invalid Groq API key. Check your key at console.groq.com.");
|
|
2730
2758
|
}
|
|
@@ -2734,7 +2762,7 @@ var init_groq = __esm({
|
|
|
2734
2762
|
if (resp.status === 429) {
|
|
2735
2763
|
throw new Error("Groq rate limit exceeded. Free tier: 28,800 seconds/day. Try again later.");
|
|
2736
2764
|
}
|
|
2737
|
-
throw new Error(`Groq
|
|
2765
|
+
throw new Error(`Groq transcription failed (HTTP ${resp.status}). Check Groq status and run /speech \u2192 Check setup.`);
|
|
2738
2766
|
}
|
|
2739
2767
|
const data = await resp.json();
|
|
2740
2768
|
return {
|
|
@@ -3202,6 +3230,20 @@ function safeError(error) {
|
|
|
3202
3230
|
const message = error instanceof Error ? error.message : "Proxy request failed";
|
|
3203
3231
|
return new Error(redactNetworkSecrets(message));
|
|
3204
3232
|
}
|
|
3233
|
+
async function approvedTargetPreflight(fetcher) {
|
|
3234
|
+
let response;
|
|
3235
|
+
try {
|
|
3236
|
+
response = await fetcher(PROXY_CONNECTIVITY_TEST_URL, {
|
|
3237
|
+
signal: AbortSignal.timeout(1e4)
|
|
3238
|
+
});
|
|
3239
|
+
if (!response.ok) throw new Error(`Connectivity check returned HTTP ${response.status}`);
|
|
3240
|
+
} finally {
|
|
3241
|
+
try {
|
|
3242
|
+
if (response?.body) await response.arrayBuffer();
|
|
3243
|
+
} catch {
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3205
3247
|
function normalizeResponse(response) {
|
|
3206
3248
|
if (response instanceof Response) return response;
|
|
3207
3249
|
const body = response.body ? Readable.toWeb(response.body) : null;
|
|
@@ -3234,7 +3276,7 @@ function copyResponse(response, body) {
|
|
|
3234
3276
|
}
|
|
3235
3277
|
return copy;
|
|
3236
3278
|
}
|
|
3237
|
-
var enabledDebug, PROXY_ENV_KEYS, ProxyValidationError, ProxyUnknownScopeError, ProxyProfileInUseError, ProxyProfileNotFoundError, ProxyProfileExistsError, ProxyRouteTestError, ProxyProfileTestError, ProxyService;
|
|
3279
|
+
var enabledDebug, PROXY_ENV_KEYS, PROXY_CONNECTIVITY_TEST_URL, ProxyValidationError, ProxyUnknownScopeError, ProxyProfileInUseError, ProxyProfileNotFoundError, ProxyProfileExistsError, ProxyRouteTestError, ProxyProfileTestError, ProxyService;
|
|
3238
3280
|
var init_proxy_service = __esm({
|
|
3239
3281
|
"src/core/network/proxy-service.ts"() {
|
|
3240
3282
|
"use strict";
|
|
@@ -3255,6 +3297,7 @@ var init_proxy_service = __esm({
|
|
|
3255
3297
|
"no_proxy",
|
|
3256
3298
|
"NODE_USE_ENV_PROXY"
|
|
3257
3299
|
];
|
|
3300
|
+
PROXY_CONNECTIVITY_TEST_URL = "https://api.ipify.org?format=json";
|
|
3258
3301
|
ProxyValidationError = class extends Error {
|
|
3259
3302
|
constructor(message, code = "PROXY_VALIDATION_ERROR") {
|
|
3260
3303
|
super(message);
|
|
@@ -3300,9 +3343,10 @@ var init_proxy_service = __esm({
|
|
|
3300
3343
|
}
|
|
3301
3344
|
};
|
|
3302
3345
|
ProxyService = class {
|
|
3303
|
-
constructor(instanceRoot, retiredLeaseTimeoutMs = 5 * 6e4, allowedNodeEnvironmentFlags = process.allowedNodeEnvironmentFlags) {
|
|
3346
|
+
constructor(instanceRoot, retiredLeaseTimeoutMs = 5 * 6e4, allowedNodeEnvironmentFlags = process.allowedNodeEnvironmentFlags, connectivityPreflight = approvedTargetPreflight) {
|
|
3304
3347
|
this.retiredLeaseTimeoutMs = retiredLeaseTimeoutMs;
|
|
3305
3348
|
this.allowedNodeEnvironmentFlags = allowedNodeEnvironmentFlags;
|
|
3349
|
+
this.connectivityPreflight = connectivityPreflight;
|
|
3306
3350
|
this.store = new ProxyStore(instanceRoot);
|
|
3307
3351
|
try {
|
|
3308
3352
|
const config = this.store.load();
|
|
@@ -3312,6 +3356,7 @@ var init_proxy_service = __esm({
|
|
|
3312
3356
|
}
|
|
3313
3357
|
retiredLeaseTimeoutMs;
|
|
3314
3358
|
allowedNodeEnvironmentFlags;
|
|
3359
|
+
connectivityPreflight;
|
|
3315
3360
|
store;
|
|
3316
3361
|
scopes = /* @__PURE__ */ new Set([
|
|
3317
3362
|
"channels.default",
|
|
@@ -3492,16 +3537,15 @@ var init_proxy_service = __esm({
|
|
|
3492
3537
|
const { profile: candidate, nextSecrets } = this.buildProfile(input2, config, secrets);
|
|
3493
3538
|
const affectedScopes = this.scopesUsingProfile(config, input2.id);
|
|
3494
3539
|
const candidateSecret = nextSecrets[input2.id];
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
}
|
|
3540
|
+
if (affectedScopes.length) {
|
|
3541
|
+
const fetcher = this.createProfileFetch(candidate, candidateSecret);
|
|
3542
|
+
try {
|
|
3543
|
+
await this.connectivityPreflight(fetcher);
|
|
3544
|
+
for (const scope of affectedScopes) await this.testers.get(scope)?.(fetcher);
|
|
3545
|
+
} catch (error) {
|
|
3546
|
+
throw new ProxyProfileTestError(input2.id, error);
|
|
3547
|
+
} finally {
|
|
3548
|
+
fetcher.destroy?.();
|
|
3505
3549
|
}
|
|
3506
3550
|
}
|
|
3507
3551
|
this.invalidatePolicyBeforeCommit(config, input2.id);
|
|
@@ -3785,7 +3829,7 @@ var init_proxy_service = __esm({
|
|
|
3785
3829
|
env.NO_PROXY = env.no_proxy = noProxy;
|
|
3786
3830
|
return env;
|
|
3787
3831
|
}
|
|
3788
|
-
async test(scope, targetUrl =
|
|
3832
|
+
async test(scope, targetUrl = PROXY_CONNECTIVITY_TEST_URL) {
|
|
3789
3833
|
if (!this.getKnownScopes().includes(scope)) throw new ProxyUnknownScopeError(scope);
|
|
3790
3834
|
let response;
|
|
3791
3835
|
try {
|
|
@@ -3806,7 +3850,7 @@ var init_proxy_service = __esm({
|
|
|
3806
3850
|
let response;
|
|
3807
3851
|
try {
|
|
3808
3852
|
fetcher = this.createTransport("services.proxyTest", `profile:${id}`);
|
|
3809
|
-
response = await fetcher(targetUrl ??
|
|
3853
|
+
response = await fetcher(targetUrl ?? PROXY_CONNECTIVITY_TEST_URL, {
|
|
3810
3854
|
signal: AbortSignal.timeout(1e4)
|
|
3811
3855
|
});
|
|
3812
3856
|
return { ok: response.ok, status: response.status };
|
|
@@ -3828,7 +3872,7 @@ var init_proxy_service = __esm({
|
|
|
3828
3872
|
const fetcher = this.createProfileFetch(profile, nextSecrets[profile.id]);
|
|
3829
3873
|
let response;
|
|
3830
3874
|
try {
|
|
3831
|
-
response = await fetcher(targetUrl ??
|
|
3875
|
+
response = await fetcher(targetUrl ?? PROXY_CONNECTIVITY_TEST_URL, {
|
|
3832
3876
|
signal: AbortSignal.timeout(1e4)
|
|
3833
3877
|
});
|
|
3834
3878
|
return { ok: response.ok, status: response.status };
|
|
@@ -3880,19 +3924,26 @@ var init_proxy_service = __esm({
|
|
|
3880
3924
|
if (!id || !this.getProfile(id)) throw new ProxyValidationError(`Proxy profile "${id ?? route}" does not exist`);
|
|
3881
3925
|
}
|
|
3882
3926
|
async testCandidateRoutes(current, candidate) {
|
|
3883
|
-
|
|
3884
|
-
|
|
3927
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3928
|
+
for (const scope of this.changedResolutionScopes(current, candidate)) {
|
|
3885
3929
|
const after = this.resolveFromConfig(scope, candidate);
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3930
|
+
const transportKey = this.candidateTransportIdentity(after);
|
|
3931
|
+
groups.set(transportKey, [...groups.get(transportKey) ?? [], { scope, route: after.route }]);
|
|
3932
|
+
}
|
|
3933
|
+
for (const group of groups.values()) {
|
|
3934
|
+
const first = group[0];
|
|
3935
|
+
const fetcher = this.createTransport(first.scope, first.route);
|
|
3936
|
+
try {
|
|
3937
|
+
await this.connectivityPreflight(fetcher);
|
|
3938
|
+
for (const { scope } of group) await this.testers.get(scope)?.(fetcher);
|
|
3939
|
+
} finally {
|
|
3940
|
+
fetcher.destroy?.();
|
|
3893
3941
|
}
|
|
3894
3942
|
}
|
|
3895
3943
|
}
|
|
3944
|
+
candidateTransportIdentity(resolution) {
|
|
3945
|
+
return JSON.stringify({ route: resolution.route, profile: resolution.profile });
|
|
3946
|
+
}
|
|
3896
3947
|
createProfileFetch(profile, secret) {
|
|
3897
3948
|
const url = proxyUrl(profile, secret);
|
|
3898
3949
|
const agent = new ProxyAgent({
|
|
@@ -4180,11 +4231,13 @@ var init_telegram_command_scopes = __esm({
|
|
|
4180
4231
|
// src/core/plugin/settings-manager.ts
|
|
4181
4232
|
var settings_manager_exports = {};
|
|
4182
4233
|
__export(settings_manager_exports, {
|
|
4234
|
+
SettingsConflictError: () => SettingsConflictError,
|
|
4183
4235
|
SettingsManager: () => SettingsManager
|
|
4184
4236
|
});
|
|
4185
4237
|
import fs21 from "fs";
|
|
4186
4238
|
import path20 from "path";
|
|
4187
|
-
import { randomUUID as randomUUID4 } from "crypto";
|
|
4239
|
+
import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
|
|
4240
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
4188
4241
|
function secureSettingsDirectory(directory) {
|
|
4189
4242
|
fs21.mkdirSync(directory, { recursive: true, mode: 448 });
|
|
4190
4243
|
fs21.chmodSync(directory, 448);
|
|
@@ -4205,13 +4258,67 @@ function repairSettingsPermissions(settingsPath, basePath) {
|
|
|
4205
4258
|
secureSettingsTree(basePath, settingsPath, false);
|
|
4206
4259
|
fs21.chmodSync(settingsPath, 384);
|
|
4207
4260
|
}
|
|
4208
|
-
function
|
|
4261
|
+
function settingsRevision(raw) {
|
|
4262
|
+
return createHash4("sha256").update(raw === null ? "missing\0" : `present\0${raw}`).digest("hex");
|
|
4263
|
+
}
|
|
4264
|
+
function parseSettings(raw) {
|
|
4265
|
+
try {
|
|
4266
|
+
const parsed = JSON.parse(raw);
|
|
4267
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
4268
|
+
} catch {
|
|
4269
|
+
return {};
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
function readSettingsSnapshot(settingsPath, basePath) {
|
|
4273
|
+
secureSettingsDirectory(basePath);
|
|
4274
|
+
if (!fs21.existsSync(settingsPath)) return { data: {}, raw: null, revision: settingsRevision(null) };
|
|
4275
|
+
repairSettingsPermissions(settingsPath, basePath);
|
|
4276
|
+
let raw;
|
|
4277
|
+
try {
|
|
4278
|
+
raw = fs21.readFileSync(settingsPath, "utf-8");
|
|
4279
|
+
} catch (error) {
|
|
4280
|
+
if (error.code === "ENOENT") {
|
|
4281
|
+
return { data: {}, raw: null, revision: settingsRevision(null) };
|
|
4282
|
+
}
|
|
4283
|
+
throw error;
|
|
4284
|
+
}
|
|
4285
|
+
return { data: parseSettings(raw), raw, revision: settingsRevision(raw) };
|
|
4286
|
+
}
|
|
4287
|
+
function assertSettingsRevision(settingsPath, basePath, expectedRevision) {
|
|
4288
|
+
const actual = readSettingsSnapshot(settingsPath, basePath).revision;
|
|
4289
|
+
if (actual !== expectedRevision) throw new SettingsConflictError();
|
|
4290
|
+
}
|
|
4291
|
+
function fsyncDirectory2(directory) {
|
|
4292
|
+
try {
|
|
4293
|
+
const directoryFd = fs21.openSync(directory, "r");
|
|
4294
|
+
try {
|
|
4295
|
+
fs21.fsyncSync(directoryFd);
|
|
4296
|
+
} finally {
|
|
4297
|
+
fs21.closeSync(directoryFd);
|
|
4298
|
+
}
|
|
4299
|
+
} catch {
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
function atomicSettingsContentWrite(settingsPath, basePath, content, expectedRevision) {
|
|
4209
4303
|
const directory = path20.dirname(settingsPath);
|
|
4210
4304
|
secureSettingsTree(basePath, settingsPath, true);
|
|
4305
|
+
if (content === null) {
|
|
4306
|
+
assertSettingsRevision(settingsPath, basePath, expectedRevision);
|
|
4307
|
+
if (!fs21.existsSync(settingsPath)) return settingsRevision(null);
|
|
4308
|
+
let replaced2 = false;
|
|
4309
|
+
try {
|
|
4310
|
+
fs21.unlinkSync(settingsPath);
|
|
4311
|
+
replaced2 = true;
|
|
4312
|
+
fsyncDirectory2(directory);
|
|
4313
|
+
return settingsRevision(null);
|
|
4314
|
+
} catch (error) {
|
|
4315
|
+
throw new AtomicSettingsWriteError(error, replaced2);
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
4211
4318
|
const temporary = `${settingsPath}.${process.pid}.${randomUUID4()}.tmp`;
|
|
4319
|
+
let replaced = false;
|
|
4212
4320
|
try {
|
|
4213
|
-
fs21.writeFileSync(temporary,
|
|
4214
|
-
`, { mode: 384, flag: "wx" });
|
|
4321
|
+
fs21.writeFileSync(temporary, content, { mode: 384, flag: "wx" });
|
|
4215
4322
|
fs21.chmodSync(temporary, 384);
|
|
4216
4323
|
const fd = fs21.openSync(temporary, "r");
|
|
4217
4324
|
try {
|
|
@@ -4219,55 +4326,274 @@ function atomicSettingsWrite(settingsPath, basePath, data) {
|
|
|
4219
4326
|
} finally {
|
|
4220
4327
|
fs21.closeSync(fd);
|
|
4221
4328
|
}
|
|
4329
|
+
assertSettingsRevision(settingsPath, basePath, expectedRevision);
|
|
4222
4330
|
fs21.renameSync(temporary, settingsPath);
|
|
4331
|
+
replaced = true;
|
|
4223
4332
|
fs21.chmodSync(settingsPath, 384);
|
|
4333
|
+
fsyncDirectory2(directory);
|
|
4334
|
+
return settingsRevision(content);
|
|
4335
|
+
} catch (error) {
|
|
4336
|
+
if (error instanceof SettingsConflictError) throw error;
|
|
4337
|
+
throw new AtomicSettingsWriteError(error, replaced);
|
|
4338
|
+
} finally {
|
|
4224
4339
|
try {
|
|
4225
|
-
|
|
4226
|
-
try {
|
|
4227
|
-
fs21.fsyncSync(directoryFd);
|
|
4228
|
-
} finally {
|
|
4229
|
-
fs21.closeSync(directoryFd);
|
|
4230
|
-
}
|
|
4340
|
+
if (fs21.existsSync(temporary)) fs21.unlinkSync(temporary);
|
|
4231
4341
|
} catch {
|
|
4232
4342
|
}
|
|
4233
|
-
}
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
function serializeSettings(data) {
|
|
4346
|
+
return `${JSON.stringify(data, null, 2)}
|
|
4347
|
+
`;
|
|
4348
|
+
}
|
|
4349
|
+
function processLiveness(pid) {
|
|
4350
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return "dead";
|
|
4351
|
+
try {
|
|
4352
|
+
process.kill(pid, 0);
|
|
4353
|
+
return "alive";
|
|
4354
|
+
} catch (error) {
|
|
4355
|
+
return error.code === "ESRCH" ? "dead" : "inaccessible";
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
function readProcessStartIdentity(pid) {
|
|
4359
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return null;
|
|
4360
|
+
if (process.platform === "linux") {
|
|
4234
4361
|
try {
|
|
4235
|
-
|
|
4362
|
+
const stat = fs21.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
4363
|
+
const endOfCommand = stat.lastIndexOf(") ");
|
|
4364
|
+
if (endOfCommand < 0) return null;
|
|
4365
|
+
const ticks = stat.slice(endOfCommand + 2).trim().split(/\s+/)[19];
|
|
4366
|
+
return /^\d+$/.test(ticks ?? "") ? `linux:${ticks}` : null;
|
|
4236
4367
|
} catch {
|
|
4368
|
+
return null;
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
if (process.platform === "win32") return null;
|
|
4372
|
+
try {
|
|
4373
|
+
const started = execFileSync4("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
4374
|
+
encoding: "utf8",
|
|
4375
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
4376
|
+
timeout: 500
|
|
4377
|
+
}).trim();
|
|
4378
|
+
return started ? `${process.platform}:${started}` : null;
|
|
4379
|
+
} catch {
|
|
4380
|
+
return null;
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
function readLockMetadata(lockPath) {
|
|
4384
|
+
try {
|
|
4385
|
+
const lockStat = fs21.lstatSync(lockPath);
|
|
4386
|
+
if (!lockStat.isDirectory() || lockStat.isSymbolicLink()) return void 0;
|
|
4387
|
+
const noFollow = fs21.constants.O_NOFOLLOW ?? 0;
|
|
4388
|
+
const directoryOnly = fs21.constants.O_DIRECTORY ?? 0;
|
|
4389
|
+
const lockFd = fs21.openSync(lockPath, fs21.constants.O_RDONLY | directoryOnly | noFollow);
|
|
4390
|
+
try {
|
|
4391
|
+
const opened = fs21.fstatSync(lockFd);
|
|
4392
|
+
if (!opened.isDirectory() || opened.dev !== lockStat.dev || opened.ino !== lockStat.ino) return void 0;
|
|
4393
|
+
fs21.fchmodSync(lockFd, 448);
|
|
4394
|
+
} finally {
|
|
4395
|
+
fs21.closeSync(lockFd);
|
|
4396
|
+
}
|
|
4397
|
+
const ownerPath = path20.join(lockPath, "owner.json");
|
|
4398
|
+
const ownerStat = fs21.lstatSync(ownerPath);
|
|
4399
|
+
if (!ownerStat.isFile() || ownerStat.isSymbolicLink()) return void 0;
|
|
4400
|
+
const fd = fs21.openSync(ownerPath, fs21.constants.O_RDONLY | noFollow);
|
|
4401
|
+
let raw;
|
|
4402
|
+
try {
|
|
4403
|
+
const opened = fs21.fstatSync(fd);
|
|
4404
|
+
if (!opened.isFile() || opened.dev !== ownerStat.dev || opened.ino !== ownerStat.ino) return void 0;
|
|
4405
|
+
fs21.fchmodSync(fd, 384);
|
|
4406
|
+
raw = fs21.readFileSync(fd, "utf-8");
|
|
4407
|
+
} finally {
|
|
4408
|
+
fs21.closeSync(fd);
|
|
4409
|
+
}
|
|
4410
|
+
const parsed = JSON.parse(raw);
|
|
4411
|
+
if (!Number.isSafeInteger(parsed.pid) || typeof parsed.owner !== "string" || !Number.isFinite(parsed.acquiredAt)) return void 0;
|
|
4412
|
+
if (parsed.processStartIdentity !== void 0 && parsed.processStartIdentity !== null && typeof parsed.processStartIdentity !== "string") return void 0;
|
|
4413
|
+
return parsed;
|
|
4414
|
+
} catch {
|
|
4415
|
+
return void 0;
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
function inspectSettingsLock(lockPath, now, previousObservation) {
|
|
4419
|
+
let stat;
|
|
4420
|
+
try {
|
|
4421
|
+
stat = fs21.lstatSync(lockPath);
|
|
4422
|
+
} catch {
|
|
4423
|
+
return { reclaim: false };
|
|
4424
|
+
}
|
|
4425
|
+
if (stat.isSymbolicLink()) return { reclaim: true, stat };
|
|
4426
|
+
const metadata = readLockMetadata(lockPath);
|
|
4427
|
+
if (metadata) {
|
|
4428
|
+
const liveness = processLiveness(metadata.pid);
|
|
4429
|
+
if (liveness === "dead") return { reclaim: true, stat };
|
|
4430
|
+
const actualIdentity = readProcessStartIdentity(metadata.pid);
|
|
4431
|
+
if (metadata.processStartIdentity && actualIdentity) {
|
|
4432
|
+
return { reclaim: metadata.processStartIdentity !== actualIdentity, stat };
|
|
4433
|
+
}
|
|
4434
|
+
const acquiredAt = metadata.acquiredAt > now ? stat.mtimeMs : metadata.acquiredAt;
|
|
4435
|
+
const lastOwnerActivity = Math.max(acquiredAt, stat.mtimeMs);
|
|
4436
|
+
return { reclaim: now - lastOwnerActivity >= SETTINGS_UNVERIFIED_OWNER_STALE_MS, stat };
|
|
4437
|
+
}
|
|
4438
|
+
const sameEntry = previousObservation?.dev === stat.dev && previousObservation.ino === stat.ino;
|
|
4439
|
+
const observation = sameEntry ? previousObservation : { dev: stat.dev, ino: stat.ino, firstSeenAt: now };
|
|
4440
|
+
const oldOnDisk = now - stat.mtimeMs >= SETTINGS_UNKNOWN_LOCK_GRACE_MS;
|
|
4441
|
+
return {
|
|
4442
|
+
reclaim: oldOnDisk || now - observation.firstSeenAt >= SETTINGS_UNKNOWN_LOCK_GRACE_MS,
|
|
4443
|
+
stat,
|
|
4444
|
+
observation
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4447
|
+
function reclaimStaleLock(lockPath, expected) {
|
|
4448
|
+
const reclaimed = `${lockPath}.stale.${process.pid}.${randomUUID4()}`;
|
|
4449
|
+
try {
|
|
4450
|
+
const current = fs21.lstatSync(lockPath);
|
|
4451
|
+
if (current.dev !== expected.dev || current.ino !== expected.ino) return false;
|
|
4452
|
+
fs21.renameSync(lockPath, reclaimed);
|
|
4453
|
+
} catch (error) {
|
|
4454
|
+
if (["ENOENT", "EEXIST"].includes(error.code ?? "")) return false;
|
|
4455
|
+
throw error;
|
|
4456
|
+
}
|
|
4457
|
+
try {
|
|
4458
|
+
fs21.rmSync(reclaimed, { recursive: true, force: true });
|
|
4459
|
+
} catch {
|
|
4460
|
+
}
|
|
4461
|
+
return true;
|
|
4462
|
+
}
|
|
4463
|
+
function delay(ms) {
|
|
4464
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
4465
|
+
}
|
|
4466
|
+
async function acquireSettingsLock(settingsPath, basePath) {
|
|
4467
|
+
secureSettingsTree(basePath, settingsPath, true);
|
|
4468
|
+
const lockPath = `${settingsPath}.lock`;
|
|
4469
|
+
const startedAt = Date.now();
|
|
4470
|
+
let unknownObservation;
|
|
4471
|
+
while (true) {
|
|
4472
|
+
try {
|
|
4473
|
+
fs21.mkdirSync(lockPath, { mode: 448 });
|
|
4474
|
+
fs21.chmodSync(lockPath, 448);
|
|
4475
|
+
const owner = randomUUID4();
|
|
4476
|
+
const metadata = {
|
|
4477
|
+
version: 1,
|
|
4478
|
+
pid: process.pid,
|
|
4479
|
+
owner,
|
|
4480
|
+
acquiredAt: Date.now(),
|
|
4481
|
+
processStartIdentity: readProcessStartIdentity(process.pid)
|
|
4482
|
+
};
|
|
4483
|
+
const ownerPath = path20.join(lockPath, "owner.json");
|
|
4484
|
+
try {
|
|
4485
|
+
const noFollow = fs21.constants.O_NOFOLLOW ?? 0;
|
|
4486
|
+
const ownerFd = fs21.openSync(
|
|
4487
|
+
ownerPath,
|
|
4488
|
+
fs21.constants.O_WRONLY | fs21.constants.O_CREAT | fs21.constants.O_EXCL | noFollow,
|
|
4489
|
+
384
|
|
4490
|
+
);
|
|
4491
|
+
try {
|
|
4492
|
+
fs21.writeFileSync(ownerFd, `${JSON.stringify(metadata)}
|
|
4493
|
+
`);
|
|
4494
|
+
fs21.fchmodSync(ownerFd, 384);
|
|
4495
|
+
fs21.fsyncSync(ownerFd);
|
|
4496
|
+
} finally {
|
|
4497
|
+
fs21.closeSync(ownerFd);
|
|
4498
|
+
}
|
|
4499
|
+
} catch (error) {
|
|
4500
|
+
try {
|
|
4501
|
+
fs21.rmSync(lockPath, { recursive: true, force: true });
|
|
4502
|
+
} catch {
|
|
4503
|
+
}
|
|
4504
|
+
throw error;
|
|
4505
|
+
}
|
|
4506
|
+
let released = false;
|
|
4507
|
+
return {
|
|
4508
|
+
release: () => {
|
|
4509
|
+
if (released) return;
|
|
4510
|
+
released = true;
|
|
4511
|
+
const current = readLockMetadata(lockPath);
|
|
4512
|
+
if (current?.owner !== owner || current.pid !== process.pid || current.processStartIdentity !== metadata.processStartIdentity) return;
|
|
4513
|
+
const releasedPath = `${lockPath}.released.${process.pid}.${randomUUID4()}`;
|
|
4514
|
+
try {
|
|
4515
|
+
fs21.renameSync(lockPath, releasedPath);
|
|
4516
|
+
fs21.rmSync(releasedPath, { recursive: true, force: true });
|
|
4517
|
+
} catch (error) {
|
|
4518
|
+
if (error.code !== "ENOENT") throw error;
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
};
|
|
4522
|
+
} catch (error) {
|
|
4523
|
+
if (error.code !== "EEXIST") throw error;
|
|
4524
|
+
const inspection = inspectSettingsLock(lockPath, Date.now(), unknownObservation);
|
|
4525
|
+
unknownObservation = inspection.observation;
|
|
4526
|
+
if (!inspection.stat) {
|
|
4527
|
+
if (Date.now() - startedAt >= SETTINGS_LOCK_WAIT_MS) {
|
|
4528
|
+
throw new Error(`Timed out waiting for plugin settings lock: ${path20.basename(path20.dirname(settingsPath))}`);
|
|
4529
|
+
}
|
|
4530
|
+
await delay(10 + Math.floor(Math.random() * 20));
|
|
4531
|
+
continue;
|
|
4532
|
+
}
|
|
4533
|
+
if (inspection.reclaim && reclaimStaleLock(lockPath, inspection.stat)) {
|
|
4534
|
+
unknownObservation = void 0;
|
|
4535
|
+
continue;
|
|
4536
|
+
}
|
|
4537
|
+
if (Date.now() - startedAt >= SETTINGS_LOCK_WAIT_MS) {
|
|
4538
|
+
throw new Error(`Timed out waiting for plugin settings lock: ${path20.basename(path20.dirname(settingsPath))}`);
|
|
4539
|
+
}
|
|
4540
|
+
await delay(10 + Math.floor(Math.random() * 20));
|
|
4237
4541
|
}
|
|
4238
4542
|
}
|
|
4239
4543
|
}
|
|
4240
|
-
|
|
4544
|
+
function cloneSettings(settings) {
|
|
4545
|
+
return JSON.parse(JSON.stringify(settings));
|
|
4546
|
+
}
|
|
4547
|
+
var SETTINGS_LOCK_WAIT_MS, SETTINGS_UNKNOWN_LOCK_GRACE_MS, SETTINGS_UNVERIFIED_OWNER_STALE_MS, SETTINGS_CONFLICT_RETRIES, AtomicSettingsWriteError, RetriableSettingsConflictError, SettingsConflictError, SettingsManager, SettingsAPIImpl;
|
|
4241
4548
|
var init_settings_manager = __esm({
|
|
4242
4549
|
"src/core/plugin/settings-manager.ts"() {
|
|
4243
4550
|
"use strict";
|
|
4244
|
-
|
|
4551
|
+
SETTINGS_LOCK_WAIT_MS = 3e4;
|
|
4552
|
+
SETTINGS_UNKNOWN_LOCK_GRACE_MS = 750;
|
|
4553
|
+
SETTINGS_UNVERIFIED_OWNER_STALE_MS = 2 * 6e4;
|
|
4554
|
+
SETTINGS_CONFLICT_RETRIES = 3;
|
|
4555
|
+
AtomicSettingsWriteError = class extends Error {
|
|
4556
|
+
constructor(original, replaced) {
|
|
4557
|
+
super(original instanceof Error ? original.message : "Settings write failed", { cause: original });
|
|
4558
|
+
this.original = original;
|
|
4559
|
+
this.replaced = replaced;
|
|
4560
|
+
this.name = "AtomicSettingsWriteError";
|
|
4561
|
+
}
|
|
4562
|
+
original;
|
|
4563
|
+
replaced;
|
|
4564
|
+
};
|
|
4565
|
+
RetriableSettingsConflictError = class extends Error {
|
|
4566
|
+
constructor(conflict) {
|
|
4567
|
+
super(conflict.message, { cause: conflict });
|
|
4568
|
+
this.conflict = conflict;
|
|
4569
|
+
this.name = "RetriableSettingsConflictError";
|
|
4570
|
+
}
|
|
4571
|
+
conflict;
|
|
4572
|
+
};
|
|
4573
|
+
SettingsConflictError = class extends Error {
|
|
4574
|
+
constructor() {
|
|
4575
|
+
super("Plugin settings changed concurrently; the attempted write was not applied");
|
|
4576
|
+
this.name = "SettingsConflictError";
|
|
4577
|
+
}
|
|
4578
|
+
};
|
|
4579
|
+
SettingsManager = class _SettingsManager {
|
|
4245
4580
|
constructor(basePath) {
|
|
4246
4581
|
this.basePath = basePath;
|
|
4247
4582
|
secureSettingsDirectory(basePath);
|
|
4248
4583
|
}
|
|
4249
4584
|
basePath;
|
|
4585
|
+
static mutationTails = /* @__PURE__ */ new Map();
|
|
4250
4586
|
/** Returns the base path for all plugin settings directories. */
|
|
4251
4587
|
getBasePath() {
|
|
4252
4588
|
return this.basePath;
|
|
4253
4589
|
}
|
|
4254
4590
|
/** Create a SettingsAPI instance scoped to a specific plugin. */
|
|
4255
4591
|
createAPI(pluginName) {
|
|
4256
|
-
|
|
4257
|
-
return new SettingsAPIImpl(settingsPath, this.basePath);
|
|
4592
|
+
return new SettingsAPIImpl(this, pluginName);
|
|
4258
4593
|
}
|
|
4259
|
-
/** Load a plugin
|
|
4594
|
+
/** Load a fresh plugin settings snapshot. Returns empty object if the file doesn't exist. */
|
|
4260
4595
|
async loadSettings(pluginName) {
|
|
4261
|
-
|
|
4262
|
-
secureSettingsDirectory(this.basePath);
|
|
4263
|
-
if (!fs21.existsSync(settingsPath)) return {};
|
|
4264
|
-
repairSettingsPermissions(settingsPath, this.basePath);
|
|
4265
|
-
try {
|
|
4266
|
-
const content = fs21.readFileSync(settingsPath, "utf-8");
|
|
4267
|
-
return JSON.parse(content);
|
|
4268
|
-
} catch {
|
|
4269
|
-
return {};
|
|
4270
|
-
}
|
|
4596
|
+
return cloneSettings(readSettingsSnapshot(this.getSettingsPath(pluginName), this.basePath).data);
|
|
4271
4597
|
}
|
|
4272
4598
|
/** Validate settings against a Zod schema. Returns valid if no schema is provided. */
|
|
4273
4599
|
validateSettings(_pluginName, settings, schema) {
|
|
@@ -4288,63 +4614,136 @@ var init_settings_manager = __esm({
|
|
|
4288
4614
|
async getPluginSettings(pluginName) {
|
|
4289
4615
|
return this.loadSettings(pluginName);
|
|
4290
4616
|
}
|
|
4617
|
+
/**
|
|
4618
|
+
* Serialize a read/prepare/persist/apply transaction for one plugin.
|
|
4619
|
+
*
|
|
4620
|
+
* The transaction holds both an in-process queue and a filesystem lock. Its
|
|
4621
|
+
* prepare callback receives a fresh disk snapshot. Persistence uses a content
|
|
4622
|
+
* revision CAS; a pre-commit conflict is retried from a new snapshot, while an
|
|
4623
|
+
* exhausted conflict aborts without applying the runtime side effect. Apply
|
|
4624
|
+
* failures restore the exact previous file content and invoke runtime rollback
|
|
4625
|
+
* before releasing the cross-process lock.
|
|
4626
|
+
*/
|
|
4627
|
+
async transactPluginSettings(pluginName, prepare) {
|
|
4628
|
+
const settingsPath = this.getSettingsPath(pluginName);
|
|
4629
|
+
return _SettingsManager.serialize(path20.resolve(settingsPath), async () => {
|
|
4630
|
+
let lastConflict;
|
|
4631
|
+
for (let attempt = 0; attempt < SETTINGS_CONFLICT_RETRIES; attempt += 1) {
|
|
4632
|
+
try {
|
|
4633
|
+
return await this.transactOnce(settingsPath, prepare);
|
|
4634
|
+
} catch (error) {
|
|
4635
|
+
if (!(error instanceof RetriableSettingsConflictError)) throw error;
|
|
4636
|
+
lastConflict = error.conflict;
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4639
|
+
throw lastConflict ?? new SettingsConflictError();
|
|
4640
|
+
});
|
|
4641
|
+
}
|
|
4291
4642
|
/** Merge updates into existing settings (shallow merge). */
|
|
4292
4643
|
async updatePluginSettings(pluginName, updates) {
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4644
|
+
await this.transactPluginSettings(pluginName, (current) => ({
|
|
4645
|
+
settings: { ...current, ...updates },
|
|
4646
|
+
result: void 0
|
|
4647
|
+
}));
|
|
4296
4648
|
}
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4649
|
+
async transactOnce(settingsPath, prepare) {
|
|
4650
|
+
const lock = await acquireSettingsLock(settingsPath, this.basePath);
|
|
4651
|
+
try {
|
|
4652
|
+
const previous = readSettingsSnapshot(settingsPath, this.basePath);
|
|
4653
|
+
const plan = await prepare(cloneSettings(previous.data));
|
|
4654
|
+
const next = cloneSettings(plan.settings);
|
|
4655
|
+
const nextContent = serializeSettings(next);
|
|
4656
|
+
const nextRevision = settingsRevision(nextContent);
|
|
4657
|
+
try {
|
|
4658
|
+
atomicSettingsContentWrite(settingsPath, this.basePath, nextContent, previous.revision);
|
|
4659
|
+
} catch (error) {
|
|
4660
|
+
if (error instanceof SettingsConflictError) throw new RetriableSettingsConflictError(error);
|
|
4661
|
+
if (error instanceof AtomicSettingsWriteError && error.replaced) {
|
|
4662
|
+
try {
|
|
4663
|
+
atomicSettingsContentWrite(settingsPath, this.basePath, previous.raw, nextRevision);
|
|
4664
|
+
} catch (restoreError) {
|
|
4665
|
+
throw new AggregateError([error.original, restoreError], "Settings persistence failed and the previous snapshot could not be restored");
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
throw error instanceof AtomicSettingsWriteError ? error.original : error;
|
|
4669
|
+
}
|
|
4670
|
+
try {
|
|
4671
|
+
await plan.apply?.();
|
|
4672
|
+
} catch (error) {
|
|
4673
|
+
const rollbackErrors = [error];
|
|
4674
|
+
try {
|
|
4675
|
+
await plan.rollback?.();
|
|
4676
|
+
} catch (rollbackError) {
|
|
4677
|
+
rollbackErrors.push(rollbackError);
|
|
4678
|
+
}
|
|
4679
|
+
try {
|
|
4680
|
+
atomicSettingsContentWrite(settingsPath, this.basePath, previous.raw, nextRevision);
|
|
4681
|
+
} catch (restoreError) {
|
|
4682
|
+
rollbackErrors.push(restoreError instanceof AtomicSettingsWriteError ? restoreError.original : restoreError);
|
|
4683
|
+
}
|
|
4684
|
+
if (rollbackErrors.length > 1) throw new AggregateError(rollbackErrors, "Settings runtime apply failed and rollback was incomplete");
|
|
4685
|
+
throw error;
|
|
4686
|
+
}
|
|
4687
|
+
return plan.result;
|
|
4688
|
+
} finally {
|
|
4689
|
+
lock.release();
|
|
4690
|
+
}
|
|
4302
4691
|
}
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4692
|
+
static async serialize(key, operation) {
|
|
4693
|
+
const previous = this.mutationTails.get(key) ?? Promise.resolve();
|
|
4694
|
+
let release;
|
|
4695
|
+
const gate = new Promise((resolve8) => {
|
|
4696
|
+
release = resolve8;
|
|
4697
|
+
});
|
|
4698
|
+
const tail = previous.catch(() => void 0).then(() => gate);
|
|
4699
|
+
this.mutationTails.set(key, tail);
|
|
4700
|
+
await previous.catch(() => void 0);
|
|
4310
4701
|
try {
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
this.cache = {};
|
|
4316
|
-
return this.cache;
|
|
4702
|
+
return await operation();
|
|
4703
|
+
} finally {
|
|
4704
|
+
release();
|
|
4705
|
+
if (this.mutationTails.get(key) === tail) this.mutationTails.delete(key);
|
|
4317
4706
|
}
|
|
4318
4707
|
}
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4708
|
+
};
|
|
4709
|
+
SettingsAPIImpl = class {
|
|
4710
|
+
constructor(manager, pluginName) {
|
|
4711
|
+
this.manager = manager;
|
|
4712
|
+
this.pluginName = pluginName;
|
|
4322
4713
|
}
|
|
4714
|
+
manager;
|
|
4715
|
+
pluginName;
|
|
4323
4716
|
async get(key) {
|
|
4324
|
-
const data = this.
|
|
4717
|
+
const data = await this.manager.loadSettings(this.pluginName);
|
|
4325
4718
|
return data[key];
|
|
4326
4719
|
}
|
|
4327
4720
|
async set(key, value) {
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4721
|
+
await this.manager.transactPluginSettings(this.pluginName, (current) => ({
|
|
4722
|
+
settings: { ...current, [key]: value },
|
|
4723
|
+
result: void 0
|
|
4724
|
+
}));
|
|
4331
4725
|
}
|
|
4332
4726
|
async getAll() {
|
|
4333
|
-
return
|
|
4727
|
+
return this.manager.loadSettings(this.pluginName);
|
|
4334
4728
|
}
|
|
4335
4729
|
async setAll(settings) {
|
|
4336
|
-
this.
|
|
4730
|
+
await this.manager.transactPluginSettings(this.pluginName, () => ({
|
|
4731
|
+
settings: { ...settings },
|
|
4732
|
+
result: void 0
|
|
4733
|
+
}));
|
|
4337
4734
|
}
|
|
4338
4735
|
async delete(key) {
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4736
|
+
await this.manager.transactPluginSettings(this.pluginName, (current) => {
|
|
4737
|
+
const next = { ...current };
|
|
4738
|
+
delete next[key];
|
|
4739
|
+
return { settings: next, result: void 0 };
|
|
4740
|
+
});
|
|
4342
4741
|
}
|
|
4343
4742
|
async clear() {
|
|
4344
|
-
this.
|
|
4743
|
+
await this.manager.transactPluginSettings(this.pluginName, () => ({ settings: {}, result: void 0 }));
|
|
4345
4744
|
}
|
|
4346
4745
|
async has(key) {
|
|
4347
|
-
const data = this.
|
|
4746
|
+
const data = await this.manager.loadSettings(this.pluginName);
|
|
4348
4747
|
return key in data;
|
|
4349
4748
|
}
|
|
4350
4749
|
};
|
|
@@ -4362,17 +4761,17 @@ __export(command_ownership_store_exports, {
|
|
|
4362
4761
|
import fs22 from "fs";
|
|
4363
4762
|
import path21 from "path";
|
|
4364
4763
|
import os5 from "os";
|
|
4365
|
-
import { createHash as
|
|
4764
|
+
import { createHash as createHash5 } from "crypto";
|
|
4366
4765
|
function telegramCommandHostId() {
|
|
4367
4766
|
let source = `${os5.hostname()}|${typeof process.getuid === "function" ? process.getuid() : "unknown"}`;
|
|
4368
4767
|
try {
|
|
4369
4768
|
source = fs22.readFileSync("/etc/machine-id", "utf8").trim() || source;
|
|
4370
4769
|
} catch {
|
|
4371
4770
|
}
|
|
4372
|
-
return
|
|
4771
|
+
return createHash5("sha256").update(source).digest("hex");
|
|
4373
4772
|
}
|
|
4374
4773
|
function telegramCommandInstanceKey(instanceRoot) {
|
|
4375
|
-
return
|
|
4774
|
+
return createHash5("sha256").update(path21.resolve(instanceRoot)).digest("hex");
|
|
4376
4775
|
}
|
|
4377
4776
|
function emptyLedger() {
|
|
4378
4777
|
return { version: STORE_VERSION, bots: {} };
|
|
@@ -5171,7 +5570,7 @@ import fs28 from "fs";
|
|
|
5171
5570
|
import path26 from "path";
|
|
5172
5571
|
import https from "https";
|
|
5173
5572
|
import os7 from "os";
|
|
5174
|
-
import { execFileSync as
|
|
5573
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
5175
5574
|
function downloadFile(url, dest, maxRedirects = 10) {
|
|
5176
5575
|
return new Promise((resolve8, reject) => {
|
|
5177
5576
|
const file = fs28.createWriteStream(dest);
|
|
@@ -5260,9 +5659,9 @@ async function ensureBinary(spec, binDir) {
|
|
|
5260
5659
|
const downloadDest = isArchive ? path26.join(resolvedBinDir, `${spec.name}.tgz`) : binPath;
|
|
5261
5660
|
await downloadFile(url, downloadDest);
|
|
5262
5661
|
if (isArchive) {
|
|
5263
|
-
const listing =
|
|
5662
|
+
const listing = execFileSync5("tar", ["-tf", downloadDest], { stdio: "pipe" }).toString().trim().split("\n").filter(Boolean);
|
|
5264
5663
|
validateTarContents(listing, resolvedBinDir);
|
|
5265
|
-
|
|
5664
|
+
execFileSync5("tar", ["-xzf", downloadDest, "-C", resolvedBinDir], { stdio: "pipe" });
|
|
5266
5665
|
try {
|
|
5267
5666
|
fs28.unlinkSync(downloadDest);
|
|
5268
5667
|
} catch {
|
|
@@ -5329,7 +5728,7 @@ var init_install_cloudflared = __esm({
|
|
|
5329
5728
|
import * as fs29 from "fs";
|
|
5330
5729
|
import * as path27 from "path";
|
|
5331
5730
|
import * as os8 from "os";
|
|
5332
|
-
import { execFileSync as
|
|
5731
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
5333
5732
|
var tunnelCheck;
|
|
5334
5733
|
var init_tunnel = __esm({
|
|
5335
5734
|
"src/core/doctor/checks/tunnel.ts"() {
|
|
@@ -5362,7 +5761,7 @@ var init_tunnel = __esm({
|
|
|
5362
5761
|
found = true;
|
|
5363
5762
|
} else {
|
|
5364
5763
|
try {
|
|
5365
|
-
|
|
5764
|
+
execFileSync6("which", ["cloudflared"], { stdio: "pipe" });
|
|
5366
5765
|
found = true;
|
|
5367
5766
|
} catch {
|
|
5368
5767
|
}
|
|
@@ -5392,48 +5791,393 @@ var init_tunnel = __esm({
|
|
|
5392
5791
|
} else {
|
|
5393
5792
|
results.push({ status: "pass", message: `Tunnel port: ${tunnelPort}` });
|
|
5394
5793
|
}
|
|
5395
|
-
return results;
|
|
5794
|
+
return results;
|
|
5795
|
+
}
|
|
5796
|
+
};
|
|
5797
|
+
}
|
|
5798
|
+
});
|
|
5799
|
+
|
|
5800
|
+
// src/core/instance/instance-registry.ts
|
|
5801
|
+
import fs30 from "fs";
|
|
5802
|
+
import path28 from "path";
|
|
5803
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
5804
|
+
function readIdFromConfig(instanceRoot) {
|
|
5805
|
+
try {
|
|
5806
|
+
const raw = JSON.parse(fs30.readFileSync(path28.join(instanceRoot, "config.json"), "utf-8"));
|
|
5807
|
+
return typeof raw.id === "string" && raw.id ? raw.id : null;
|
|
5808
|
+
} catch {
|
|
5809
|
+
return null;
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
var InstanceRegistry;
|
|
5813
|
+
var init_instance_registry = __esm({
|
|
5814
|
+
"src/core/instance/instance-registry.ts"() {
|
|
5815
|
+
"use strict";
|
|
5816
|
+
InstanceRegistry = class {
|
|
5817
|
+
constructor(registryPath) {
|
|
5818
|
+
this.registryPath = registryPath;
|
|
5819
|
+
}
|
|
5820
|
+
registryPath;
|
|
5821
|
+
data = { version: 1, instances: {} };
|
|
5822
|
+
/** Load the registry from disk. If the file is missing or corrupt, starts fresh. */
|
|
5823
|
+
load() {
|
|
5824
|
+
try {
|
|
5825
|
+
const raw = fs30.readFileSync(this.registryPath, "utf-8");
|
|
5826
|
+
const parsed = JSON.parse(raw);
|
|
5827
|
+
if (parsed.version === 1 && parsed.instances) {
|
|
5828
|
+
this.data = parsed;
|
|
5829
|
+
this.deduplicate();
|
|
5830
|
+
}
|
|
5831
|
+
} catch {
|
|
5832
|
+
}
|
|
5833
|
+
}
|
|
5834
|
+
/** Remove duplicate entries that point to the same root, keeping the first one */
|
|
5835
|
+
deduplicate() {
|
|
5836
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5837
|
+
const toRemove = [];
|
|
5838
|
+
for (const [id, entry] of Object.entries(this.data.instances)) {
|
|
5839
|
+
if (seen.has(entry.root)) {
|
|
5840
|
+
toRemove.push(id);
|
|
5841
|
+
} else {
|
|
5842
|
+
seen.add(entry.root);
|
|
5843
|
+
}
|
|
5844
|
+
}
|
|
5845
|
+
if (toRemove.length > 0) {
|
|
5846
|
+
for (const id of toRemove) delete this.data.instances[id];
|
|
5847
|
+
this.save();
|
|
5848
|
+
}
|
|
5849
|
+
}
|
|
5850
|
+
/** Persist the registry to disk, creating parent directories if needed. */
|
|
5851
|
+
save() {
|
|
5852
|
+
const dir = path28.dirname(this.registryPath);
|
|
5853
|
+
fs30.mkdirSync(dir, { recursive: true });
|
|
5854
|
+
fs30.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
|
|
5855
|
+
}
|
|
5856
|
+
/** Add or update an instance entry in the registry. Does not persist — call save() after. */
|
|
5857
|
+
register(id, root) {
|
|
5858
|
+
this.data.instances[id] = { id, root };
|
|
5859
|
+
}
|
|
5860
|
+
/** Remove an instance entry. Does not persist — call save() after. */
|
|
5861
|
+
remove(id) {
|
|
5862
|
+
delete this.data.instances[id];
|
|
5863
|
+
}
|
|
5864
|
+
/** Look up an instance by its ID. */
|
|
5865
|
+
get(id) {
|
|
5866
|
+
return this.data.instances[id];
|
|
5867
|
+
}
|
|
5868
|
+
/** Look up an instance by its root directory path. */
|
|
5869
|
+
getByRoot(root) {
|
|
5870
|
+
return Object.values(this.data.instances).find((e) => e.root === root);
|
|
5871
|
+
}
|
|
5872
|
+
/** Returns all registered instances. */
|
|
5873
|
+
list() {
|
|
5874
|
+
return Object.values(this.data.instances);
|
|
5875
|
+
}
|
|
5876
|
+
/** Returns `baseId` if available, otherwise appends `-2`, `-3`, etc. until unique. */
|
|
5877
|
+
uniqueId(baseId) {
|
|
5878
|
+
if (!this.data.instances[baseId]) return baseId;
|
|
5879
|
+
let n = 2;
|
|
5880
|
+
while (this.data.instances[`${baseId}-${n}`]) n++;
|
|
5881
|
+
return `${baseId}-${n}`;
|
|
5882
|
+
}
|
|
5883
|
+
/**
|
|
5884
|
+
* Resolve the authoritative id for an instance root.
|
|
5885
|
+
*
|
|
5886
|
+
* config.json is the source of truth. If the registry entry disagrees with
|
|
5887
|
+
* config.json, the registry is updated to match. If neither exists, a fresh
|
|
5888
|
+
* UUID is generated and registered (but NOT written to config.json — the
|
|
5889
|
+
* caller must call initInstanceFiles to persist it).
|
|
5890
|
+
*
|
|
5891
|
+
* Returns the resolved id and whether the registry was mutated (so callers
|
|
5892
|
+
* can decide whether to persist with save()).
|
|
5893
|
+
*/
|
|
5894
|
+
resolveId(instanceRoot) {
|
|
5895
|
+
const configId = readIdFromConfig(instanceRoot);
|
|
5896
|
+
const entry = this.getByRoot(instanceRoot);
|
|
5897
|
+
if (entry && configId && entry.id !== configId) {
|
|
5898
|
+
this.remove(entry.id);
|
|
5899
|
+
this.register(configId, instanceRoot);
|
|
5900
|
+
return { id: configId, registryUpdated: true };
|
|
5901
|
+
}
|
|
5902
|
+
const id = configId ?? entry?.id ?? randomUUID5();
|
|
5903
|
+
if (!this.getByRoot(instanceRoot)) {
|
|
5904
|
+
this.register(id, instanceRoot);
|
|
5905
|
+
return { id, registryUpdated: true };
|
|
5906
|
+
}
|
|
5907
|
+
return { id, registryUpdated: false };
|
|
5396
5908
|
}
|
|
5397
5909
|
};
|
|
5398
5910
|
}
|
|
5399
5911
|
});
|
|
5400
5912
|
|
|
5913
|
+
// src/core/instance/daemon-identity.ts
|
|
5914
|
+
import fs31 from "fs";
|
|
5915
|
+
import path29 from "path";
|
|
5916
|
+
function daemonIdentityPath(pidPath) {
|
|
5917
|
+
return `${pidPath}.identity`;
|
|
5918
|
+
}
|
|
5919
|
+
function processStartTicks(pid) {
|
|
5920
|
+
if (process.platform !== "linux") return null;
|
|
5921
|
+
try {
|
|
5922
|
+
const stat = fs31.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
5923
|
+
const endOfCommand = stat.lastIndexOf(") ");
|
|
5924
|
+
if (endOfCommand < 0) return null;
|
|
5925
|
+
const value = stat.slice(endOfCommand + 2).trim().split(/\s+/)[19];
|
|
5926
|
+
return /^\d+$/.test(value ?? "") ? value : null;
|
|
5927
|
+
} catch {
|
|
5928
|
+
return null;
|
|
5929
|
+
}
|
|
5930
|
+
}
|
|
5931
|
+
function writeDaemonIdentity(pidPath, pid) {
|
|
5932
|
+
const instanceRoot = path29.resolve(path29.dirname(pidPath));
|
|
5933
|
+
const marker = {
|
|
5934
|
+
version: 1,
|
|
5935
|
+
pid,
|
|
5936
|
+
instanceRoot,
|
|
5937
|
+
instanceId: readIdFromConfig(instanceRoot),
|
|
5938
|
+
processStartTicks: processStartTicks(pid)
|
|
5939
|
+
};
|
|
5940
|
+
fs31.writeFileSync(daemonIdentityPath(pidPath), JSON.stringify(marker), { mode: 384 });
|
|
5941
|
+
}
|
|
5942
|
+
function removeDaemonIdentity(pidPath) {
|
|
5943
|
+
try {
|
|
5944
|
+
fs31.unlinkSync(daemonIdentityPath(pidPath));
|
|
5945
|
+
} catch {
|
|
5946
|
+
}
|
|
5947
|
+
}
|
|
5948
|
+
function verifyDaemonIdentity(pidPath, pid, expectedInstanceRoot) {
|
|
5949
|
+
try {
|
|
5950
|
+
const marker = JSON.parse(fs31.readFileSync(daemonIdentityPath(pidPath), "utf8"));
|
|
5951
|
+
const expectedRoot = path29.resolve(expectedInstanceRoot);
|
|
5952
|
+
if (marker.version !== 1 || marker.pid !== pid || marker.instanceRoot !== expectedRoot) return false;
|
|
5953
|
+
const expectedId = readIdFromConfig(expectedRoot);
|
|
5954
|
+
if (expectedId && marker.instanceId !== expectedId) return false;
|
|
5955
|
+
const currentStartTicks = processStartTicks(pid);
|
|
5956
|
+
if (process.platform === "linux") {
|
|
5957
|
+
if (!marker.processStartTicks || !currentStartTicks || marker.processStartTicks !== currentStartTicks) return false;
|
|
5958
|
+
}
|
|
5959
|
+
return true;
|
|
5960
|
+
} catch {
|
|
5961
|
+
return false;
|
|
5962
|
+
}
|
|
5963
|
+
}
|
|
5964
|
+
var init_daemon_identity = __esm({
|
|
5965
|
+
"src/core/instance/daemon-identity.ts"() {
|
|
5966
|
+
"use strict";
|
|
5967
|
+
init_instance_registry();
|
|
5968
|
+
}
|
|
5969
|
+
});
|
|
5970
|
+
|
|
5401
5971
|
// src/core/doctor/checks/proxy.ts
|
|
5402
|
-
|
|
5972
|
+
import fs32 from "fs";
|
|
5973
|
+
import path30 from "path";
|
|
5974
|
+
function activeCallerProxyVariables() {
|
|
5975
|
+
return [...ROUTING_PROXY_KEYS].filter((key) => process.env[key] !== void 0 && process.env[key] !== "");
|
|
5976
|
+
}
|
|
5977
|
+
function isProcessAlive2(pid) {
|
|
5978
|
+
try {
|
|
5979
|
+
process.kill(pid, 0);
|
|
5980
|
+
return true;
|
|
5981
|
+
} catch (error) {
|
|
5982
|
+
return error.code === "EPERM";
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5985
|
+
function inspectDaemonProxyEnvironment(pidPath, expectedInstanceRoot = path30.dirname(pidPath)) {
|
|
5986
|
+
let pid;
|
|
5987
|
+
try {
|
|
5988
|
+
pid = Number(fs32.readFileSync(pidPath, "utf8").trim());
|
|
5989
|
+
} catch {
|
|
5990
|
+
return { state: "not-running", variableNames: [], source: "none" };
|
|
5991
|
+
}
|
|
5992
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || !isProcessAlive2(pid)) {
|
|
5993
|
+
return { state: "not-running", variableNames: [], source: "none" };
|
|
5994
|
+
}
|
|
5995
|
+
if (!verifyDaemonIdentity(pidPath, pid, expectedInstanceRoot)) {
|
|
5996
|
+
return { state: "unavailable", variableNames: [], source: "none" };
|
|
5997
|
+
}
|
|
5998
|
+
if (pid === process.pid) {
|
|
5999
|
+
return { state: "known", variableNames: activeCallerProxyVariables(), source: "current-process" };
|
|
6000
|
+
}
|
|
6001
|
+
if (process.platform !== "linux") {
|
|
6002
|
+
return { state: "unavailable", variableNames: [], source: "none" };
|
|
6003
|
+
}
|
|
6004
|
+
try {
|
|
6005
|
+
const names = fs32.readFileSync(`/proc/${pid}/environ`).toString("utf8").split("\0").filter((entry) => entry.includes("=")).map((entry) => entry.slice(0, entry.indexOf("="))).filter((key) => ROUTING_PROXY_KEYS.has(key));
|
|
6006
|
+
return { state: "known", variableNames: [...new Set(names)].sort(), source: "proc" };
|
|
6007
|
+
} catch {
|
|
6008
|
+
return { state: "unavailable", variableNames: [], source: "none" };
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
var ROUTING_PROXY_KEYS, proxyCheck;
|
|
5403
6012
|
var init_proxy = __esm({
|
|
5404
6013
|
"src/core/doctor/checks/proxy.ts"() {
|
|
5405
6014
|
"use strict";
|
|
5406
6015
|
init_proxy_service();
|
|
5407
6016
|
init_proxy_store();
|
|
6017
|
+
init_daemon_identity();
|
|
6018
|
+
ROUTING_PROXY_KEYS = new Set(PROXY_ENV_KEYS.filter((key) => !["NO_PROXY", "no_proxy", "NODE_USE_ENV_PROXY"].includes(key)));
|
|
5408
6019
|
proxyCheck = {
|
|
5409
|
-
name: "
|
|
6020
|
+
name: "Network proxy",
|
|
5410
6021
|
order: 85,
|
|
5411
6022
|
async run(ctx) {
|
|
5412
6023
|
try {
|
|
5413
6024
|
if (ctx.dataDir) new ProxyStore(ctx.dataDir).load();
|
|
5414
6025
|
} catch (error) {
|
|
5415
|
-
if (error instanceof ProxyStoreCorruptError) return [{ status: "fail", message: `${error.message}. Network
|
|
6026
|
+
if (error instanceof ProxyStoreCorruptError) return [{ status: "fail", message: `${error.message}. Network traffic is blocked for safety. Inspect the quarantined file and last known-good copy before recovery.` }];
|
|
5416
6027
|
throw error;
|
|
5417
6028
|
}
|
|
5418
|
-
const
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
6029
|
+
const callerVariables = activeCallerProxyVariables();
|
|
6030
|
+
const daemon = ctx.daemonProxyEnvironment ?? inspectDaemonProxyEnvironment(ctx.pidPath ?? "", ctx.dataDir);
|
|
6031
|
+
if (daemon.state === "known" && daemon.variableNames.length) {
|
|
6032
|
+
return [{
|
|
6033
|
+
status: "warn",
|
|
6034
|
+
message: `Compatibility mode: the running OpenACP daemon has proxy variables (${daemon.variableNames.join(", ")}). Scoped routes still apply, but \u201CUse host proxy settings\u201D can use these variables. Remove only the OpenACP wrapper or service override when migration is complete.`
|
|
6035
|
+
}];
|
|
6036
|
+
}
|
|
6037
|
+
if (daemon.state === "known") {
|
|
6038
|
+
const shellNote = callerVariables.length && daemon.source !== "current-process" ? ` Current command shell has proxy variables (${callerVariables.join(", ")}), but they are not present in the running daemon.` : "";
|
|
6039
|
+
return [{ status: "pass", message: `Scoped routing is active; the running daemon has no proxy variables.${shellNote}` }];
|
|
6040
|
+
}
|
|
6041
|
+
if (callerVariables.length) {
|
|
6042
|
+
const daemonState = daemon.state === "not-running" ? "No running daemon was available to inspect" : "The running daemon environment could not be inspected on this platform";
|
|
6043
|
+
return [{
|
|
6044
|
+
status: "warn",
|
|
6045
|
+
message: `Current command shell has proxy variables (${callerVariables.join(", ")}). ${daemonState}, so daemon compatibility mode was not inferred. A daemon started from this shell may inherit them.`
|
|
6046
|
+
}];
|
|
5424
6047
|
}
|
|
5425
6048
|
return [{
|
|
5426
|
-
status: "
|
|
5427
|
-
message:
|
|
6049
|
+
status: "pass",
|
|
6050
|
+
message: daemon.state === "not-running" ? "Current command shell has no proxy variables; no running daemon environment was available" : "Current command shell has no proxy variables; the running daemon environment could not be inspected, so compatibility mode was not inferred"
|
|
5428
6051
|
}];
|
|
5429
6052
|
}
|
|
5430
6053
|
};
|
|
5431
6054
|
}
|
|
5432
6055
|
});
|
|
5433
6056
|
|
|
6057
|
+
// src/plugins/speech/local-readiness.ts
|
|
6058
|
+
import { statSync } from "fs";
|
|
6059
|
+
function getLocalWhisperReadiness(settings, options = {}) {
|
|
6060
|
+
const env = options.env ?? process.env;
|
|
6061
|
+
const envScriptPath = env[LOCAL_WHISPER_SCRIPT_PATH_ENV]?.trim();
|
|
6062
|
+
const effectiveSettings = envScriptPath ? { ...settings, localWhisperScriptPath: envScriptPath } : settings;
|
|
6063
|
+
const scriptPath = readLocalWhisperSettings(effectiveSettings).scriptPath;
|
|
6064
|
+
const commandAvailable = options.commandAvailable ?? commandExists;
|
|
6065
|
+
let script;
|
|
6066
|
+
try {
|
|
6067
|
+
const stat = statSync(scriptPath);
|
|
6068
|
+
script = !stat.isFile() ? "not-file" : (stat.mode & 73) === 0 ? "not-executable" : "ready";
|
|
6069
|
+
} catch {
|
|
6070
|
+
script = "missing";
|
|
6071
|
+
}
|
|
6072
|
+
const runtimeReady = commandAvailable("uv") || commandAvailable("python3");
|
|
6073
|
+
return { ready: script === "ready" && runtimeReady, script, runtimeReady };
|
|
6074
|
+
}
|
|
6075
|
+
var LOCAL_WHISPER_SCRIPT_PATH_ENV;
|
|
6076
|
+
var init_local_readiness = __esm({
|
|
6077
|
+
"src/plugins/speech/local-readiness.ts"() {
|
|
6078
|
+
"use strict";
|
|
6079
|
+
init_agent_dependencies();
|
|
6080
|
+
init_native_stt();
|
|
6081
|
+
LOCAL_WHISPER_SCRIPT_PATH_ENV = "OPENACP_SPEECH_LOCAL_WHISPER_SCRIPT_PATH";
|
|
6082
|
+
}
|
|
6083
|
+
});
|
|
6084
|
+
|
|
6085
|
+
// src/core/network/proxy-labels.ts
|
|
6086
|
+
function humanizeIdentifier(value) {
|
|
6087
|
+
const words = value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[._-]+/g, " ").trim();
|
|
6088
|
+
return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "Unknown traffic";
|
|
6089
|
+
}
|
|
6090
|
+
function proxyCategoryLabel(category) {
|
|
6091
|
+
return CATEGORY_LABELS[category] ?? humanizeIdentifier(category);
|
|
6092
|
+
}
|
|
6093
|
+
function proxyScopeLabel(scope) {
|
|
6094
|
+
if (SCOPE_LABELS[scope]) return SCOPE_LABELS[scope];
|
|
6095
|
+
const [category, ...rest] = scope.split(".");
|
|
6096
|
+
const detail = humanizeIdentifier(rest.join("."));
|
|
6097
|
+
return rest.length ? `${proxyCategoryLabel(category)} \xB7 ${detail}` : humanizeIdentifier(scope);
|
|
6098
|
+
}
|
|
6099
|
+
var CATEGORY_LABELS, SCOPE_LABELS;
|
|
6100
|
+
var init_proxy_labels = __esm({
|
|
6101
|
+
"src/core/network/proxy-labels.ts"() {
|
|
6102
|
+
"use strict";
|
|
6103
|
+
CATEGORY_LABELS = {
|
|
6104
|
+
channels: "Messaging connectors",
|
|
6105
|
+
agents: "Coding agents",
|
|
6106
|
+
services: "OpenACP services",
|
|
6107
|
+
plugins: "Plugins"
|
|
6108
|
+
};
|
|
6109
|
+
SCOPE_LABELS = {
|
|
6110
|
+
global: "Default for all traffic",
|
|
6111
|
+
"channels.default": "Other messaging connectors",
|
|
6112
|
+
"channels.telegram": "Telegram",
|
|
6113
|
+
"agents.default": "Other coding agents",
|
|
6114
|
+
"agents.codex": "Codex",
|
|
6115
|
+
"agents.cursor": "Cursor",
|
|
6116
|
+
"services.default": "Other OpenACP services",
|
|
6117
|
+
"services.npmUpdate": "OpenACP updates",
|
|
6118
|
+
"services.agentRegistry": "Agent catalog",
|
|
6119
|
+
"services.pluginInstaller": "Plugin installation",
|
|
6120
|
+
"services.speech": "Groq transcription",
|
|
6121
|
+
"services.speechDownloads": "Local speech model downloads",
|
|
6122
|
+
"plugins.default": "Plugin network traffic"
|
|
6123
|
+
};
|
|
6124
|
+
}
|
|
6125
|
+
});
|
|
6126
|
+
|
|
6127
|
+
// src/core/doctor/checks/speech.ts
|
|
6128
|
+
import path31 from "path";
|
|
6129
|
+
var groqRoute, modelDownloadRoute, speechCheck;
|
|
6130
|
+
var init_speech = __esm({
|
|
6131
|
+
"src/core/doctor/checks/speech.ts"() {
|
|
6132
|
+
"use strict";
|
|
6133
|
+
init_settings_manager();
|
|
6134
|
+
init_native_stt();
|
|
6135
|
+
init_local_readiness();
|
|
6136
|
+
init_groq();
|
|
6137
|
+
init_proxy_labels();
|
|
6138
|
+
groqRoute = proxyScopeLabel("services.speech");
|
|
6139
|
+
modelDownloadRoute = proxyScopeLabel("services.speechDownloads");
|
|
6140
|
+
speechCheck = {
|
|
6141
|
+
name: "Speech-to-text",
|
|
6142
|
+
order: 84,
|
|
6143
|
+
async run(ctx) {
|
|
6144
|
+
const settings = await new SettingsManager(path31.join(ctx.pluginsDir, "data")).loadSettings("@openacp/speech");
|
|
6145
|
+
const legacyGroqKey = typeof settings.groqApiKey === "string" && settings.groqApiKey.trim().length > 0;
|
|
6146
|
+
const method = Object.prototype.hasOwnProperty.call(settings, "sttProvider") ? settings.sttProvider : legacyGroqKey ? "groq" : void 0;
|
|
6147
|
+
if (method !== LOCAL_WHISPER_PROVIDER && method !== "groq") {
|
|
6148
|
+
return [{ status: "pass", message: "Off (optional); no voice messages are sent for transcription" }];
|
|
6149
|
+
}
|
|
6150
|
+
if (method === LOCAL_WHISPER_PROVIDER) {
|
|
6151
|
+
const readiness = getLocalWhisperReadiness(settings);
|
|
6152
|
+
if (readiness.ready) {
|
|
6153
|
+
return [{ status: "pass", message: `Local transcription selected; runtime ready. The model downloads on first use using the ${modelDownloadRoute} route` }];
|
|
6154
|
+
}
|
|
6155
|
+
const issue = readiness.script === "missing" ? "configured transcription executable missing" : readiness.script === "not-file" ? "configured transcription executable is not a file" : readiness.script === "not-executable" ? "configured transcription file is not executable" : "Python 3 or uv not found";
|
|
6156
|
+
return [{
|
|
6157
|
+
status: "warn",
|
|
6158
|
+
message: `Local transcription selected but setup is not ready (${issue}). Restore or authorize the configured executable, or install Python 3/uv; model downloads use the ${modelDownloadRoute} route`
|
|
6159
|
+
}];
|
|
6160
|
+
}
|
|
6161
|
+
const key = typeof settings.groqApiKey === "string" ? settings.groqApiKey.trim() : "";
|
|
6162
|
+
if (!key) return [{ status: "warn", message: "Groq cloud transcription selected; API key not set. Add it securely in Settings \u2192 Speech-to-text" }];
|
|
6163
|
+
try {
|
|
6164
|
+
const provider = new GroqSTT(key, void 0, ctx.fetchForScope("services.speech"));
|
|
6165
|
+
const result = await provider.checkAccess(AbortSignal.timeout(8e3));
|
|
6166
|
+
return [{
|
|
6167
|
+
status: result.ok ? "pass" : "warn",
|
|
6168
|
+
message: result.ok ? `Groq cloud transcription selected; API key saved (hidden) and access verified using the ${groqRoute} route` : `Groq cloud transcription selected; API key saved (hidden), but access is not ready: ${result.message} Route: ${groqRoute}`
|
|
6169
|
+
}];
|
|
6170
|
+
} catch {
|
|
6171
|
+
return [{ status: "warn", message: `Groq cloud transcription selected; API key saved (hidden), but the ${groqRoute} route could not start an access check` }];
|
|
6172
|
+
}
|
|
6173
|
+
}
|
|
6174
|
+
};
|
|
6175
|
+
}
|
|
6176
|
+
});
|
|
6177
|
+
|
|
5434
6178
|
// src/core/doctor/index.ts
|
|
5435
|
-
import * as
|
|
5436
|
-
import * as
|
|
6179
|
+
import * as fs33 from "fs";
|
|
6180
|
+
import * as path32 from "path";
|
|
5437
6181
|
var ALL_CHECKS, CHECK_TIMEOUT_MS, DoctorEngine;
|
|
5438
6182
|
var init_doctor = __esm({
|
|
5439
6183
|
"src/core/doctor/index.ts"() {
|
|
@@ -5449,6 +6193,7 @@ var init_doctor = __esm({
|
|
|
5449
6193
|
init_daemon();
|
|
5450
6194
|
init_tunnel();
|
|
5451
6195
|
init_proxy();
|
|
6196
|
+
init_speech();
|
|
5452
6197
|
ALL_CHECKS = [
|
|
5453
6198
|
configCheck,
|
|
5454
6199
|
agentsCheck,
|
|
@@ -5458,6 +6203,7 @@ var init_doctor = __esm({
|
|
|
5458
6203
|
pluginsCheck,
|
|
5459
6204
|
daemonCheck,
|
|
5460
6205
|
tunnelCheck,
|
|
6206
|
+
speechCheck,
|
|
5461
6207
|
proxyCheck
|
|
5462
6208
|
];
|
|
5463
6209
|
CHECK_TIMEOUT_MS = 1e4;
|
|
@@ -5526,28 +6272,29 @@ var init_doctor = __esm({
|
|
|
5526
6272
|
/** Constructs the shared context used by all checks — loads config if available. */
|
|
5527
6273
|
async buildContext() {
|
|
5528
6274
|
const dataDir = this.dataDir;
|
|
5529
|
-
const configPath = process.env.OPENACP_CONFIG_PATH ||
|
|
6275
|
+
const configPath = process.env.OPENACP_CONFIG_PATH || path32.join(dataDir, "config.json");
|
|
5530
6276
|
let config = null;
|
|
5531
6277
|
let rawConfig = null;
|
|
5532
6278
|
try {
|
|
5533
|
-
const content =
|
|
6279
|
+
const content = fs33.readFileSync(configPath, "utf-8");
|
|
5534
6280
|
rawConfig = JSON.parse(content);
|
|
5535
6281
|
const cm = new ConfigManager(configPath);
|
|
5536
6282
|
await cm.load();
|
|
5537
6283
|
config = cm.get();
|
|
5538
6284
|
} catch {
|
|
5539
6285
|
}
|
|
5540
|
-
const logsDir = config ? expandHome2(config.logging.logDir) :
|
|
6286
|
+
const logsDir = config ? expandHome2(config.logging.logDir) : path32.join(dataDir, "logs");
|
|
5541
6287
|
return {
|
|
5542
6288
|
config,
|
|
5543
6289
|
rawConfig,
|
|
5544
6290
|
configPath,
|
|
5545
6291
|
dataDir,
|
|
5546
|
-
sessionsPath:
|
|
5547
|
-
pidPath:
|
|
5548
|
-
portFilePath:
|
|
5549
|
-
pluginsDir:
|
|
6292
|
+
sessionsPath: path32.join(dataDir, "sessions.json"),
|
|
6293
|
+
pidPath: path32.join(dataDir, "openacp.pid"),
|
|
6294
|
+
portFilePath: path32.join(dataDir, "api.port"),
|
|
6295
|
+
pluginsDir: path32.join(dataDir, "plugins"),
|
|
5550
6296
|
logsDir,
|
|
6297
|
+
daemonProxyEnvironment: inspectDaemonProxyEnvironment(path32.join(dataDir, "openacp.pid"), dataDir),
|
|
5551
6298
|
fetchForScope: (scope) => this.proxyService.createFetch(scope)
|
|
5552
6299
|
};
|
|
5553
6300
|
}
|
|
@@ -5555,119 +6302,6 @@ var init_doctor = __esm({
|
|
|
5555
6302
|
}
|
|
5556
6303
|
});
|
|
5557
6304
|
|
|
5558
|
-
// src/core/instance/instance-registry.ts
|
|
5559
|
-
import fs32 from "fs";
|
|
5560
|
-
import path30 from "path";
|
|
5561
|
-
import { randomUUID as randomUUID5 } from "crypto";
|
|
5562
|
-
function readIdFromConfig(instanceRoot) {
|
|
5563
|
-
try {
|
|
5564
|
-
const raw = JSON.parse(fs32.readFileSync(path30.join(instanceRoot, "config.json"), "utf-8"));
|
|
5565
|
-
return typeof raw.id === "string" && raw.id ? raw.id : null;
|
|
5566
|
-
} catch {
|
|
5567
|
-
return null;
|
|
5568
|
-
}
|
|
5569
|
-
}
|
|
5570
|
-
var InstanceRegistry;
|
|
5571
|
-
var init_instance_registry = __esm({
|
|
5572
|
-
"src/core/instance/instance-registry.ts"() {
|
|
5573
|
-
"use strict";
|
|
5574
|
-
InstanceRegistry = class {
|
|
5575
|
-
constructor(registryPath) {
|
|
5576
|
-
this.registryPath = registryPath;
|
|
5577
|
-
}
|
|
5578
|
-
registryPath;
|
|
5579
|
-
data = { version: 1, instances: {} };
|
|
5580
|
-
/** Load the registry from disk. If the file is missing or corrupt, starts fresh. */
|
|
5581
|
-
load() {
|
|
5582
|
-
try {
|
|
5583
|
-
const raw = fs32.readFileSync(this.registryPath, "utf-8");
|
|
5584
|
-
const parsed = JSON.parse(raw);
|
|
5585
|
-
if (parsed.version === 1 && parsed.instances) {
|
|
5586
|
-
this.data = parsed;
|
|
5587
|
-
this.deduplicate();
|
|
5588
|
-
}
|
|
5589
|
-
} catch {
|
|
5590
|
-
}
|
|
5591
|
-
}
|
|
5592
|
-
/** Remove duplicate entries that point to the same root, keeping the first one */
|
|
5593
|
-
deduplicate() {
|
|
5594
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5595
|
-
const toRemove = [];
|
|
5596
|
-
for (const [id, entry] of Object.entries(this.data.instances)) {
|
|
5597
|
-
if (seen.has(entry.root)) {
|
|
5598
|
-
toRemove.push(id);
|
|
5599
|
-
} else {
|
|
5600
|
-
seen.add(entry.root);
|
|
5601
|
-
}
|
|
5602
|
-
}
|
|
5603
|
-
if (toRemove.length > 0) {
|
|
5604
|
-
for (const id of toRemove) delete this.data.instances[id];
|
|
5605
|
-
this.save();
|
|
5606
|
-
}
|
|
5607
|
-
}
|
|
5608
|
-
/** Persist the registry to disk, creating parent directories if needed. */
|
|
5609
|
-
save() {
|
|
5610
|
-
const dir = path30.dirname(this.registryPath);
|
|
5611
|
-
fs32.mkdirSync(dir, { recursive: true });
|
|
5612
|
-
fs32.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
|
|
5613
|
-
}
|
|
5614
|
-
/** Add or update an instance entry in the registry. Does not persist — call save() after. */
|
|
5615
|
-
register(id, root) {
|
|
5616
|
-
this.data.instances[id] = { id, root };
|
|
5617
|
-
}
|
|
5618
|
-
/** Remove an instance entry. Does not persist — call save() after. */
|
|
5619
|
-
remove(id) {
|
|
5620
|
-
delete this.data.instances[id];
|
|
5621
|
-
}
|
|
5622
|
-
/** Look up an instance by its ID. */
|
|
5623
|
-
get(id) {
|
|
5624
|
-
return this.data.instances[id];
|
|
5625
|
-
}
|
|
5626
|
-
/** Look up an instance by its root directory path. */
|
|
5627
|
-
getByRoot(root) {
|
|
5628
|
-
return Object.values(this.data.instances).find((e) => e.root === root);
|
|
5629
|
-
}
|
|
5630
|
-
/** Returns all registered instances. */
|
|
5631
|
-
list() {
|
|
5632
|
-
return Object.values(this.data.instances);
|
|
5633
|
-
}
|
|
5634
|
-
/** Returns `baseId` if available, otherwise appends `-2`, `-3`, etc. until unique. */
|
|
5635
|
-
uniqueId(baseId) {
|
|
5636
|
-
if (!this.data.instances[baseId]) return baseId;
|
|
5637
|
-
let n = 2;
|
|
5638
|
-
while (this.data.instances[`${baseId}-${n}`]) n++;
|
|
5639
|
-
return `${baseId}-${n}`;
|
|
5640
|
-
}
|
|
5641
|
-
/**
|
|
5642
|
-
* Resolve the authoritative id for an instance root.
|
|
5643
|
-
*
|
|
5644
|
-
* config.json is the source of truth. If the registry entry disagrees with
|
|
5645
|
-
* config.json, the registry is updated to match. If neither exists, a fresh
|
|
5646
|
-
* UUID is generated and registered (but NOT written to config.json — the
|
|
5647
|
-
* caller must call initInstanceFiles to persist it).
|
|
5648
|
-
*
|
|
5649
|
-
* Returns the resolved id and whether the registry was mutated (so callers
|
|
5650
|
-
* can decide whether to persist with save()).
|
|
5651
|
-
*/
|
|
5652
|
-
resolveId(instanceRoot) {
|
|
5653
|
-
const configId = readIdFromConfig(instanceRoot);
|
|
5654
|
-
const entry = this.getByRoot(instanceRoot);
|
|
5655
|
-
if (entry && configId && entry.id !== configId) {
|
|
5656
|
-
this.remove(entry.id);
|
|
5657
|
-
this.register(configId, instanceRoot);
|
|
5658
|
-
return { id: configId, registryUpdated: true };
|
|
5659
|
-
}
|
|
5660
|
-
const id = configId ?? entry?.id ?? randomUUID5();
|
|
5661
|
-
if (!this.getByRoot(instanceRoot)) {
|
|
5662
|
-
this.register(id, instanceRoot);
|
|
5663
|
-
return { id, registryUpdated: true };
|
|
5664
|
-
}
|
|
5665
|
-
return { id, registryUpdated: false };
|
|
5666
|
-
}
|
|
5667
|
-
};
|
|
5668
|
-
}
|
|
5669
|
-
});
|
|
5670
|
-
|
|
5671
6305
|
// src/core/plugin/terminal-io.ts
|
|
5672
6306
|
import * as clack from "@clack/prompts";
|
|
5673
6307
|
function isCancel(value) {
|
|
@@ -5731,17 +6365,18 @@ var install_context_exports = {};
|
|
|
5731
6365
|
__export(install_context_exports, {
|
|
5732
6366
|
createInstallContext: () => createInstallContext
|
|
5733
6367
|
});
|
|
5734
|
-
import
|
|
6368
|
+
import path35 from "path";
|
|
5735
6369
|
function createInstallContext(opts) {
|
|
5736
6370
|
const { pluginName, settingsManager, basePath, instanceRoot } = opts;
|
|
5737
|
-
const dataDir =
|
|
6371
|
+
const dataDir = path35.join(basePath, pluginName, "data");
|
|
5738
6372
|
return {
|
|
5739
6373
|
pluginName,
|
|
5740
6374
|
terminal: createTerminalIO(),
|
|
5741
6375
|
settings: settingsManager.createAPI(pluginName),
|
|
5742
6376
|
dataDir,
|
|
5743
6377
|
log: log.child({ plugin: pluginName }),
|
|
5744
|
-
instanceRoot
|
|
6378
|
+
instanceRoot,
|
|
6379
|
+
transactSettings: (prepare) => settingsManager.transactPluginSettings(pluginName, prepare)
|
|
5745
6380
|
};
|
|
5746
6381
|
}
|
|
5747
6382
|
var init_install_context = __esm({
|
|
@@ -5762,14 +6397,14 @@ __export(api_client_exports, {
|
|
|
5762
6397
|
waitForApiReady: () => waitForApiReady,
|
|
5763
6398
|
waitForPortFile: () => waitForPortFile
|
|
5764
6399
|
});
|
|
5765
|
-
import * as
|
|
5766
|
-
import * as
|
|
6400
|
+
import * as fs36 from "fs";
|
|
6401
|
+
import * as path36 from "path";
|
|
5767
6402
|
import { setTimeout as sleep } from "timers/promises";
|
|
5768
6403
|
function defaultPortFile(root) {
|
|
5769
|
-
return
|
|
6404
|
+
return path36.join(root, "api.port");
|
|
5770
6405
|
}
|
|
5771
6406
|
function defaultSecretFile(root) {
|
|
5772
|
-
return
|
|
6407
|
+
return path36.join(root, "api-secret");
|
|
5773
6408
|
}
|
|
5774
6409
|
async function waitForPortFile(portFilePath, timeoutMs = 5e3, intervalMs = 100) {
|
|
5775
6410
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -5803,7 +6438,7 @@ async function waitForApiReady(instanceRoot, expectedInstanceId, timeoutMs = 1e4
|
|
|
5803
6438
|
function readApiPort(portFilePath, instanceRoot) {
|
|
5804
6439
|
const filePath = portFilePath ?? defaultPortFile(instanceRoot);
|
|
5805
6440
|
try {
|
|
5806
|
-
const content =
|
|
6441
|
+
const content = fs36.readFileSync(filePath, "utf-8").trim();
|
|
5807
6442
|
const port = parseInt(content, 10);
|
|
5808
6443
|
return isNaN(port) ? null : port;
|
|
5809
6444
|
} catch {
|
|
@@ -5813,7 +6448,7 @@ function readApiPort(portFilePath, instanceRoot) {
|
|
|
5813
6448
|
function readApiSecret(secretFilePath, instanceRoot) {
|
|
5814
6449
|
const filePath = secretFilePath ?? defaultSecretFile(instanceRoot);
|
|
5815
6450
|
try {
|
|
5816
|
-
const content =
|
|
6451
|
+
const content = fs36.readFileSync(filePath, "utf-8").trim();
|
|
5817
6452
|
return content || null;
|
|
5818
6453
|
} catch {
|
|
5819
6454
|
return null;
|
|
@@ -5822,7 +6457,7 @@ function readApiSecret(secretFilePath, instanceRoot) {
|
|
|
5822
6457
|
function removeStalePortFile(portFilePath, instanceRoot) {
|
|
5823
6458
|
const filePath = portFilePath ?? defaultPortFile(instanceRoot);
|
|
5824
6459
|
try {
|
|
5825
|
-
|
|
6460
|
+
fs36.unlinkSync(filePath);
|
|
5826
6461
|
} catch {
|
|
5827
6462
|
}
|
|
5828
6463
|
}
|
|
@@ -5959,8 +6594,8 @@ var init_notification = __esm({
|
|
|
5959
6594
|
});
|
|
5960
6595
|
|
|
5961
6596
|
// src/plugins/file-service/file-service.ts
|
|
5962
|
-
import
|
|
5963
|
-
import
|
|
6597
|
+
import fs38 from "fs";
|
|
6598
|
+
import path39 from "path";
|
|
5964
6599
|
import { OggOpusDecoder } from "ogg-opus-decoder";
|
|
5965
6600
|
import wav from "node-wav";
|
|
5966
6601
|
function classifyMime(mimeType) {
|
|
@@ -6017,14 +6652,14 @@ var init_file_service = __esm({
|
|
|
6017
6652
|
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
6018
6653
|
let removed = 0;
|
|
6019
6654
|
try {
|
|
6020
|
-
const entries = await
|
|
6655
|
+
const entries = await fs38.promises.readdir(this.baseDir, { withFileTypes: true });
|
|
6021
6656
|
for (const entry of entries) {
|
|
6022
6657
|
if (!entry.isDirectory()) continue;
|
|
6023
|
-
const dirPath =
|
|
6658
|
+
const dirPath = path39.join(this.baseDir, entry.name);
|
|
6024
6659
|
try {
|
|
6025
|
-
const stat = await
|
|
6660
|
+
const stat = await fs38.promises.stat(dirPath);
|
|
6026
6661
|
if (stat.mtimeMs < cutoff) {
|
|
6027
|
-
await
|
|
6662
|
+
await fs38.promises.rm(dirPath, { recursive: true, force: true });
|
|
6028
6663
|
removed++;
|
|
6029
6664
|
}
|
|
6030
6665
|
} catch {
|
|
@@ -6043,11 +6678,11 @@ var init_file_service = __esm({
|
|
|
6043
6678
|
* so the user-facing name is not lost.
|
|
6044
6679
|
*/
|
|
6045
6680
|
async saveFile(sessionId, fileName, data, mimeType) {
|
|
6046
|
-
const sessionDir =
|
|
6047
|
-
await
|
|
6681
|
+
const sessionDir = path39.join(this.baseDir, sessionId);
|
|
6682
|
+
await fs38.promises.mkdir(sessionDir, { recursive: true });
|
|
6048
6683
|
const safeName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
6049
|
-
const filePath =
|
|
6050
|
-
await
|
|
6684
|
+
const filePath = path39.join(sessionDir, safeName);
|
|
6685
|
+
await fs38.promises.writeFile(filePath, data);
|
|
6051
6686
|
return {
|
|
6052
6687
|
type: classifyMime(mimeType),
|
|
6053
6688
|
filePath,
|
|
@@ -6062,14 +6697,14 @@ var init_file_service = __esm({
|
|
|
6062
6697
|
*/
|
|
6063
6698
|
async resolveFile(filePath) {
|
|
6064
6699
|
try {
|
|
6065
|
-
const stat = await
|
|
6700
|
+
const stat = await fs38.promises.stat(filePath);
|
|
6066
6701
|
if (!stat.isFile()) return null;
|
|
6067
|
-
const ext =
|
|
6702
|
+
const ext = path39.extname(filePath).toLowerCase();
|
|
6068
6703
|
const mimeType = EXT_TO_MIME[ext] || "application/octet-stream";
|
|
6069
6704
|
return {
|
|
6070
6705
|
type: classifyMime(mimeType),
|
|
6071
6706
|
filePath,
|
|
6072
|
-
fileName:
|
|
6707
|
+
fileName: path39.basename(filePath),
|
|
6073
6708
|
mimeType,
|
|
6074
6709
|
size: stat.size
|
|
6075
6710
|
};
|
|
@@ -6408,6 +7043,9 @@ import fastifyRateLimit from "@fastify/rate-limit";
|
|
|
6408
7043
|
import { serializerCompiler, validatorCompiler } from "fastify-type-provider-zod";
|
|
6409
7044
|
async function createApiServer(options) {
|
|
6410
7045
|
const app = Fastify({ logger: options.logger ?? false, forceCloseConnections: true });
|
|
7046
|
+
app.addHook("onClose", async () => {
|
|
7047
|
+
if (options.tokenStore) await options.tokenStore.close();
|
|
7048
|
+
});
|
|
6411
7049
|
app.setValidatorCompiler(validatorCompiler);
|
|
6412
7050
|
app.setSerializerCompiler(serializerCompiler);
|
|
6413
7051
|
app.addHook("onRequest", async (request, reply) => {
|
|
@@ -6712,8 +7350,8 @@ data: ${JSON.stringify(data)}
|
|
|
6712
7350
|
});
|
|
6713
7351
|
|
|
6714
7352
|
// src/plugins/api-server/static-server.ts
|
|
6715
|
-
import * as
|
|
6716
|
-
import * as
|
|
7353
|
+
import * as fs39 from "fs";
|
|
7354
|
+
import * as path40 from "path";
|
|
6717
7355
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6718
7356
|
var MIME_TYPES, StaticServer;
|
|
6719
7357
|
var init_static_server = __esm({
|
|
@@ -6737,16 +7375,16 @@ var init_static_server = __esm({
|
|
|
6737
7375
|
this.uiDir = uiDir;
|
|
6738
7376
|
if (!this.uiDir) {
|
|
6739
7377
|
const __filename = fileURLToPath2(import.meta.url);
|
|
6740
|
-
const candidate =
|
|
6741
|
-
if (
|
|
7378
|
+
const candidate = path40.resolve(path40.dirname(__filename), "../../ui/dist");
|
|
7379
|
+
if (fs39.existsSync(path40.join(candidate, "index.html"))) {
|
|
6742
7380
|
this.uiDir = candidate;
|
|
6743
7381
|
}
|
|
6744
7382
|
if (!this.uiDir) {
|
|
6745
|
-
const publishCandidate =
|
|
6746
|
-
|
|
7383
|
+
const publishCandidate = path40.resolve(
|
|
7384
|
+
path40.dirname(__filename),
|
|
6747
7385
|
"../ui"
|
|
6748
7386
|
);
|
|
6749
|
-
if (
|
|
7387
|
+
if (fs39.existsSync(path40.join(publishCandidate, "index.html"))) {
|
|
6750
7388
|
this.uiDir = publishCandidate;
|
|
6751
7389
|
}
|
|
6752
7390
|
}
|
|
@@ -6764,23 +7402,23 @@ var init_static_server = __esm({
|
|
|
6764
7402
|
serve(req, res) {
|
|
6765
7403
|
if (!this.uiDir) return false;
|
|
6766
7404
|
const urlPath = (req.url || "/").split("?")[0];
|
|
6767
|
-
const safePath =
|
|
6768
|
-
const filePath =
|
|
6769
|
-
if (!filePath.startsWith(this.uiDir +
|
|
7405
|
+
const safePath = path40.normalize(urlPath);
|
|
7406
|
+
const filePath = path40.join(this.uiDir, safePath);
|
|
7407
|
+
if (!filePath.startsWith(this.uiDir + path40.sep) && filePath !== this.uiDir)
|
|
6770
7408
|
return false;
|
|
6771
7409
|
let realFilePath;
|
|
6772
7410
|
try {
|
|
6773
|
-
realFilePath =
|
|
7411
|
+
realFilePath = fs39.realpathSync(filePath);
|
|
6774
7412
|
} catch {
|
|
6775
7413
|
realFilePath = null;
|
|
6776
7414
|
}
|
|
6777
7415
|
if (realFilePath !== null) {
|
|
6778
|
-
const realUiDir =
|
|
6779
|
-
if (!realFilePath.startsWith(realUiDir +
|
|
7416
|
+
const realUiDir = fs39.realpathSync(this.uiDir);
|
|
7417
|
+
if (!realFilePath.startsWith(realUiDir + path40.sep) && realFilePath !== realUiDir)
|
|
6780
7418
|
return false;
|
|
6781
7419
|
}
|
|
6782
|
-
if (realFilePath !== null &&
|
|
6783
|
-
const ext =
|
|
7420
|
+
if (realFilePath !== null && fs39.existsSync(realFilePath) && fs39.statSync(realFilePath).isFile()) {
|
|
7421
|
+
const ext = path40.extname(filePath);
|
|
6784
7422
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
6785
7423
|
const isHashed = /\.[a-zA-Z0-9]{8,}\.(js|css)$/.test(filePath);
|
|
6786
7424
|
const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "no-cache";
|
|
@@ -6788,16 +7426,16 @@ var init_static_server = __esm({
|
|
|
6788
7426
|
"Content-Type": contentType,
|
|
6789
7427
|
"Cache-Control": cacheControl
|
|
6790
7428
|
});
|
|
6791
|
-
|
|
7429
|
+
fs39.createReadStream(realFilePath).pipe(res);
|
|
6792
7430
|
return true;
|
|
6793
7431
|
}
|
|
6794
|
-
const indexPath =
|
|
6795
|
-
if (
|
|
7432
|
+
const indexPath = path40.join(this.uiDir, "index.html");
|
|
7433
|
+
if (fs39.existsSync(indexPath)) {
|
|
6796
7434
|
res.writeHead(200, {
|
|
6797
7435
|
"Content-Type": "text/html; charset=utf-8",
|
|
6798
7436
|
"Cache-Control": "no-cache"
|
|
6799
7437
|
});
|
|
6800
|
-
|
|
7438
|
+
fs39.createReadStream(indexPath).pipe(res);
|
|
6801
7439
|
return true;
|
|
6802
7440
|
}
|
|
6803
7441
|
return false;
|
|
@@ -6821,23 +7459,28 @@ var init_speech_service = __esm({
|
|
|
6821
7459
|
providerFactory;
|
|
6822
7460
|
factoryOwnedSTT = /* @__PURE__ */ new Set();
|
|
6823
7461
|
factoryOwnedTTS = /* @__PURE__ */ new Set();
|
|
7462
|
+
runtimeRevision = 0;
|
|
6824
7463
|
/** Set a factory function that can recreate providers from config (for hot-reload) */
|
|
6825
7464
|
setProviderFactory(factory) {
|
|
6826
7465
|
this.providerFactory = factory;
|
|
7466
|
+
this.runtimeRevision += 1;
|
|
6827
7467
|
}
|
|
6828
7468
|
/** Register an STT provider by name. Overwrites any existing provider with the same name. */
|
|
6829
7469
|
registerSTTProvider(name, provider) {
|
|
6830
7470
|
this.sttProviders.set(name, provider);
|
|
6831
7471
|
this.factoryOwnedSTT.delete(name);
|
|
7472
|
+
this.runtimeRevision += 1;
|
|
6832
7473
|
}
|
|
6833
7474
|
/** Register a TTS provider by name. Called by external TTS plugins (e.g. msedge-tts-plugin). */
|
|
6834
7475
|
registerTTSProvider(name, provider) {
|
|
6835
7476
|
this.ttsProviders.set(name, provider);
|
|
6836
7477
|
this.factoryOwnedTTS.delete(name);
|
|
7478
|
+
this.runtimeRevision += 1;
|
|
6837
7479
|
}
|
|
6838
7480
|
/** Remove a TTS provider — called by external plugins on teardown. */
|
|
6839
7481
|
unregisterTTSProvider(name) {
|
|
6840
7482
|
this.ttsProviders.delete(name);
|
|
7483
|
+
this.runtimeRevision += 1;
|
|
6841
7484
|
}
|
|
6842
7485
|
/** Returns true if an STT provider is configured and has credentials. */
|
|
6843
7486
|
isSTTAvailable() {
|
|
@@ -6862,7 +7505,7 @@ var init_speech_service = __esm({
|
|
|
6862
7505
|
async transcribe(audioBuffer, mimeType, options) {
|
|
6863
7506
|
const providerName = this.config.stt.provider;
|
|
6864
7507
|
if (!providerName || !this.config.stt.providers[providerName]?.apiKey) {
|
|
6865
|
-
throw new Error("
|
|
7508
|
+
throw new Error("Speech-to-text is off or not ready. Open Settings \u2192 Speech-to-text or run /speech.");
|
|
6866
7509
|
}
|
|
6867
7510
|
const provider = this.sttProviders.get(providerName);
|
|
6868
7511
|
if (!provider) {
|
|
@@ -6889,6 +7532,77 @@ var init_speech_service = __esm({
|
|
|
6889
7532
|
/** Replace the active config without rebuilding providers. Use `refreshProviders` to also rebuild. */
|
|
6890
7533
|
updateConfig(config) {
|
|
6891
7534
|
this.config = config;
|
|
7535
|
+
this.runtimeRevision += 1;
|
|
7536
|
+
}
|
|
7537
|
+
/**
|
|
7538
|
+
* Build and validate a complete factory-owned provider replacement without
|
|
7539
|
+
* changing the active runtime. The returned commit is revision-checked and
|
|
7540
|
+
* preserves providers registered by external plugins. Rollback restores the
|
|
7541
|
+
* exact config, maps, ownership sets, and provider object references captured
|
|
7542
|
+
* immediately before commit.
|
|
7543
|
+
*/
|
|
7544
|
+
prepareProviderRefresh(newConfig) {
|
|
7545
|
+
if (!this.providerFactory) {
|
|
7546
|
+
const baseRevision2 = this.runtimeRevision;
|
|
7547
|
+
let previousConfig;
|
|
7548
|
+
let committed2 = false;
|
|
7549
|
+
return {
|
|
7550
|
+
commit: () => {
|
|
7551
|
+
if (committed2 || this.runtimeRevision !== baseRevision2) throw new Error("Speech runtime changed while provider refresh was being prepared");
|
|
7552
|
+
previousConfig = this.config;
|
|
7553
|
+
this.config = newConfig;
|
|
7554
|
+
this.runtimeRevision = baseRevision2 + 1;
|
|
7555
|
+
committed2 = true;
|
|
7556
|
+
},
|
|
7557
|
+
rollback: () => {
|
|
7558
|
+
if (!committed2 || previousConfig === void 0) return;
|
|
7559
|
+
if (this.runtimeRevision !== baseRevision2 + 1) throw new Error("Speech runtime changed before provider refresh rollback");
|
|
7560
|
+
this.config = previousConfig;
|
|
7561
|
+
this.runtimeRevision = baseRevision2;
|
|
7562
|
+
committed2 = false;
|
|
7563
|
+
}
|
|
7564
|
+
};
|
|
7565
|
+
}
|
|
7566
|
+
const baseRevision = this.runtimeRevision;
|
|
7567
|
+
const built = this.providerFactory(newConfig);
|
|
7568
|
+
let previous;
|
|
7569
|
+
let committed = false;
|
|
7570
|
+
return {
|
|
7571
|
+
commit: () => {
|
|
7572
|
+
if (committed || this.runtimeRevision !== baseRevision) throw new Error("Speech runtime changed while provider refresh was being prepared");
|
|
7573
|
+
previous = {
|
|
7574
|
+
config: this.config,
|
|
7575
|
+
sttProviders: this.sttProviders,
|
|
7576
|
+
ttsProviders: this.ttsProviders,
|
|
7577
|
+
factoryOwnedSTT: this.factoryOwnedSTT,
|
|
7578
|
+
factoryOwnedTTS: this.factoryOwnedTTS
|
|
7579
|
+
};
|
|
7580
|
+
const nextSTT = new Map(this.sttProviders);
|
|
7581
|
+
const nextTTS = new Map(this.ttsProviders);
|
|
7582
|
+
for (const name of this.factoryOwnedSTT) nextSTT.delete(name);
|
|
7583
|
+
for (const name of this.factoryOwnedTTS) nextTTS.delete(name);
|
|
7584
|
+
for (const [name, provider] of built.stt) nextSTT.set(name, provider);
|
|
7585
|
+
for (const [name, provider] of built.tts) nextTTS.set(name, provider);
|
|
7586
|
+
this.config = newConfig;
|
|
7587
|
+
this.sttProviders = nextSTT;
|
|
7588
|
+
this.ttsProviders = nextTTS;
|
|
7589
|
+
this.factoryOwnedSTT = new Set(built.stt.keys());
|
|
7590
|
+
this.factoryOwnedTTS = new Set(built.tts.keys());
|
|
7591
|
+
this.runtimeRevision = baseRevision + 1;
|
|
7592
|
+
committed = true;
|
|
7593
|
+
},
|
|
7594
|
+
rollback: () => {
|
|
7595
|
+
if (!committed || !previous) return;
|
|
7596
|
+
if (this.runtimeRevision !== baseRevision + 1) throw new Error("Speech runtime changed before provider refresh rollback");
|
|
7597
|
+
this.config = previous.config;
|
|
7598
|
+
this.sttProviders = previous.sttProviders;
|
|
7599
|
+
this.ttsProviders = previous.ttsProviders;
|
|
7600
|
+
this.factoryOwnedSTT = previous.factoryOwnedSTT;
|
|
7601
|
+
this.factoryOwnedTTS = previous.factoryOwnedTTS;
|
|
7602
|
+
this.runtimeRevision = baseRevision;
|
|
7603
|
+
committed = false;
|
|
7604
|
+
}
|
|
7605
|
+
};
|
|
6892
7606
|
}
|
|
6893
7607
|
/**
|
|
6894
7608
|
* Reloads TTS and STT providers from a new config snapshot.
|
|
@@ -6898,22 +7612,7 @@ var init_speech_service = __esm({
|
|
|
6898
7612
|
* (e.g. from `@openacp/msedge-tts-plugin`) are preserved rather than discarded.
|
|
6899
7613
|
*/
|
|
6900
7614
|
refreshProviders(newConfig) {
|
|
6901
|
-
|
|
6902
|
-
this.config = newConfig;
|
|
6903
|
-
return;
|
|
6904
|
-
}
|
|
6905
|
-
const built = this.providerFactory(newConfig);
|
|
6906
|
-
const nextSTT = new Map(this.sttProviders);
|
|
6907
|
-
const nextTTS = new Map(this.ttsProviders);
|
|
6908
|
-
for (const name of this.factoryOwnedSTT) nextSTT.delete(name);
|
|
6909
|
-
for (const name of this.factoryOwnedTTS) nextTTS.delete(name);
|
|
6910
|
-
for (const [name, provider] of built.stt) nextSTT.set(name, provider);
|
|
6911
|
-
for (const [name, provider] of built.tts) nextTTS.set(name, provider);
|
|
6912
|
-
this.config = newConfig;
|
|
6913
|
-
this.sttProviders = nextSTT;
|
|
6914
|
-
this.ttsProviders = nextTTS;
|
|
6915
|
-
this.factoryOwnedSTT = new Set(built.stt.keys());
|
|
6916
|
-
this.factoryOwnedTTS = new Set(built.tts.keys());
|
|
7615
|
+
this.prepareProviderRefresh(newConfig).commit();
|
|
6917
7616
|
}
|
|
6918
7617
|
};
|
|
6919
7618
|
}
|
|
@@ -6930,8 +7629,8 @@ var init_exports = __esm({
|
|
|
6930
7629
|
});
|
|
6931
7630
|
|
|
6932
7631
|
// src/plugins/context/context-cache.ts
|
|
6933
|
-
import * as
|
|
6934
|
-
import * as
|
|
7632
|
+
import * as fs40 from "fs";
|
|
7633
|
+
import * as path41 from "path";
|
|
6935
7634
|
import * as crypto2 from "crypto";
|
|
6936
7635
|
var DEFAULT_TTL_MS, ContextCache;
|
|
6937
7636
|
var init_context_cache = __esm({
|
|
@@ -6942,7 +7641,7 @@ var init_context_cache = __esm({
|
|
|
6942
7641
|
constructor(cacheDir, ttlMs = DEFAULT_TTL_MS) {
|
|
6943
7642
|
this.cacheDir = cacheDir;
|
|
6944
7643
|
this.ttlMs = ttlMs;
|
|
6945
|
-
|
|
7644
|
+
fs40.mkdirSync(cacheDir, { recursive: true });
|
|
6946
7645
|
}
|
|
6947
7646
|
cacheDir;
|
|
6948
7647
|
ttlMs;
|
|
@@ -6950,23 +7649,23 @@ var init_context_cache = __esm({
|
|
|
6950
7649
|
return crypto2.createHash("sha256").update(`${repoPath}:${queryKey}`).digest("hex").slice(0, 16);
|
|
6951
7650
|
}
|
|
6952
7651
|
filePath(repoPath, queryKey) {
|
|
6953
|
-
return
|
|
7652
|
+
return path41.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
|
|
6954
7653
|
}
|
|
6955
7654
|
get(repoPath, queryKey) {
|
|
6956
7655
|
const fp = this.filePath(repoPath, queryKey);
|
|
6957
7656
|
try {
|
|
6958
|
-
const stat =
|
|
7657
|
+
const stat = fs40.statSync(fp);
|
|
6959
7658
|
if (Date.now() - stat.mtimeMs > this.ttlMs) {
|
|
6960
|
-
|
|
7659
|
+
fs40.unlinkSync(fp);
|
|
6961
7660
|
return null;
|
|
6962
7661
|
}
|
|
6963
|
-
return JSON.parse(
|
|
7662
|
+
return JSON.parse(fs40.readFileSync(fp, "utf-8"));
|
|
6964
7663
|
} catch {
|
|
6965
7664
|
return null;
|
|
6966
7665
|
}
|
|
6967
7666
|
}
|
|
6968
7667
|
set(repoPath, queryKey, result) {
|
|
6969
|
-
|
|
7668
|
+
fs40.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
|
|
6970
7669
|
}
|
|
6971
7670
|
};
|
|
6972
7671
|
}
|
|
@@ -7092,7 +7791,7 @@ var init_context_provider = __esm({
|
|
|
7092
7791
|
});
|
|
7093
7792
|
|
|
7094
7793
|
// src/plugins/context/entire/checkpoint-reader.ts
|
|
7095
|
-
import { execFileSync as
|
|
7794
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
7096
7795
|
var ENTIRE_BRANCH, CHECKPOINT_ID_RE, SESSION_ID_RE, CheckpointReader;
|
|
7097
7796
|
var init_checkpoint_reader = __esm({
|
|
7098
7797
|
"src/plugins/context/entire/checkpoint-reader.ts"() {
|
|
@@ -7112,7 +7811,7 @@ var init_checkpoint_reader = __esm({
|
|
|
7112
7811
|
*/
|
|
7113
7812
|
git(...args) {
|
|
7114
7813
|
try {
|
|
7115
|
-
return
|
|
7814
|
+
return execFileSync8("git", ["-C", this.repoPath, ...args], {
|
|
7116
7815
|
encoding: "utf-8"
|
|
7117
7816
|
}).trim();
|
|
7118
7817
|
} catch {
|
|
@@ -7940,8 +8639,8 @@ function formatToolSummary(name, rawInput, displaySummary) {
|
|
|
7940
8639
|
}
|
|
7941
8640
|
if (lowerName === "grep") {
|
|
7942
8641
|
const pattern = args.pattern ?? "";
|
|
7943
|
-
const
|
|
7944
|
-
return pattern ? `\u{1F50D} Grep "${pattern}"${
|
|
8642
|
+
const path42 = args.path ?? "";
|
|
8643
|
+
return pattern ? `\u{1F50D} Grep "${pattern}"${path42 ? ` in ${path42}` : ""}` : `\u{1F527} ${name}`;
|
|
7945
8644
|
}
|
|
7946
8645
|
if (lowerName === "glob") {
|
|
7947
8646
|
const pattern = args.pattern ?? "";
|
|
@@ -7977,8 +8676,8 @@ function formatToolTitle(name, rawInput, displayTitle) {
|
|
|
7977
8676
|
}
|
|
7978
8677
|
if (lowerName === "grep") {
|
|
7979
8678
|
const pattern = args.pattern ?? "";
|
|
7980
|
-
const
|
|
7981
|
-
return pattern ? `"${pattern}"${
|
|
8679
|
+
const path42 = args.path ?? "";
|
|
8680
|
+
return pattern ? `"${pattern}"${path42 ? ` in ${path42}` : ""}` : name;
|
|
7982
8681
|
}
|
|
7983
8682
|
if (lowerName === "glob") {
|
|
7984
8683
|
return String(args.pattern ?? name);
|
|
@@ -8456,9 +9155,9 @@ var init_send_queue = __esm({
|
|
|
8456
9155
|
const interval = this.getInterval(item.category);
|
|
8457
9156
|
const lastExec = item.category ? this.lastCategoryExec.get(item.category) ?? 0 : this.lastExec;
|
|
8458
9157
|
const elapsed = Date.now() - lastExec;
|
|
8459
|
-
const
|
|
9158
|
+
const delay2 = Math.max(0, interval - elapsed);
|
|
8460
9159
|
this.processing = true;
|
|
8461
|
-
setTimeout(() => void this.processNext(),
|
|
9160
|
+
setTimeout(() => void this.processNext(), delay2);
|
|
8462
9161
|
}
|
|
8463
9162
|
getInterval(category) {
|
|
8464
9163
|
if (category && this.config.categoryIntervals?.[category] != null) {
|
|
@@ -11157,6 +11856,8 @@ import { InlineKeyboard as InlineKeyboard7 } from "grammy";
|
|
|
11157
11856
|
async function buildSettingsKeyboard(core) {
|
|
11158
11857
|
const fields = getSafeFields();
|
|
11159
11858
|
const kb = new InlineKeyboard7();
|
|
11859
|
+
kb.text("\u{1F399} Speech-to-text", settingsCommandCallback("/speech")).row();
|
|
11860
|
+
kb.text("\u{1F310} Network proxy", settingsCommandCallback("/proxy")).row();
|
|
11160
11861
|
for (const field of fields) {
|
|
11161
11862
|
const value = getConfigValue(core.configManager.get(), field.path);
|
|
11162
11863
|
const label = formatFieldLabel(field, value);
|
|
@@ -11168,8 +11869,6 @@ async function buildSettingsKeyboard(core) {
|
|
|
11168
11869
|
kb.text(`${label}`, `s:input:${field.path}`).row();
|
|
11169
11870
|
}
|
|
11170
11871
|
}
|
|
11171
|
-
kb.text("\u{1F310} Proxy Management", settingsCommandCallback("/proxy")).row();
|
|
11172
|
-
kb.text("\u{1F399} Speech-to-Text", settingsCommandCallback("/speech")).row();
|
|
11173
11872
|
kb.text("\u25C0\uFE0F Back to Menu", "s:back");
|
|
11174
11873
|
return kb;
|
|
11175
11874
|
}
|
|
@@ -11193,7 +11892,7 @@ function formatFieldLabel(field, value) {
|
|
|
11193
11892
|
async function handleSettings(ctx, core) {
|
|
11194
11893
|
const kb = await buildSettingsKeyboard(core);
|
|
11195
11894
|
await ctx.reply(`<b>\u2699\uFE0F Settings</b>
|
|
11196
|
-
|
|
11895
|
+
Choose a section. Changes that need a restart are marked.`, {
|
|
11197
11896
|
parse_mode: "HTML",
|
|
11198
11897
|
reply_markup: kb
|
|
11199
11898
|
});
|
|
@@ -11264,7 +11963,7 @@ Select a value:`, {
|
|
|
11264
11963
|
}
|
|
11265
11964
|
try {
|
|
11266
11965
|
await ctx.editMessageText(`<b>\u2699\uFE0F Settings</b>
|
|
11267
|
-
|
|
11966
|
+
Choose a section. Changes that need a restart are marked.`, {
|
|
11268
11967
|
parse_mode: "HTML",
|
|
11269
11968
|
reply_markup: await buildSettingsKeyboard(core)
|
|
11270
11969
|
});
|
|
@@ -11321,7 +12020,7 @@ Choose an action:`, {
|
|
|
11321
12020
|
}
|
|
11322
12021
|
try {
|
|
11323
12022
|
await ctx.editMessageText(`<b>\u2699\uFE0F Settings</b>
|
|
11324
|
-
|
|
12023
|
+
Choose a section. Changes that need a restart are marked.`, {
|
|
11325
12024
|
parse_mode: "HTML",
|
|
11326
12025
|
reply_markup: await buildSettingsKeyboard(core)
|
|
11327
12026
|
});
|
|
@@ -11340,30 +12039,107 @@ var init_settings = __esm({
|
|
|
11340
12039
|
}
|
|
11341
12040
|
});
|
|
11342
12041
|
|
|
12042
|
+
// src/core/doctor/format.ts
|
|
12043
|
+
function countNoun(count, singular, plural) {
|
|
12044
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
12045
|
+
}
|
|
12046
|
+
function doctorHeadline(summary) {
|
|
12047
|
+
if (summary.failed === 1) return "1 failure needs attention";
|
|
12048
|
+
if (summary.failed > 1) return `${summary.failed} failures need attention`;
|
|
12049
|
+
if (summary.warnings > 0) return `${countNoun(summary.warnings, "warning", "warnings")} to review`;
|
|
12050
|
+
return "All checks passed";
|
|
12051
|
+
}
|
|
12052
|
+
function doctorSummary(summary, separator = ", ") {
|
|
12053
|
+
const counts = [
|
|
12054
|
+
`${summary.passed} passed`,
|
|
12055
|
+
countNoun(summary.warnings, "warning", "warnings"),
|
|
12056
|
+
countNoun(summary.failed, "failure", "failures")
|
|
12057
|
+
];
|
|
12058
|
+
if (summary.fixed) counts.push(countNoun(summary.fixed, "fix", "fixes"));
|
|
12059
|
+
return counts.join(separator);
|
|
12060
|
+
}
|
|
12061
|
+
var init_format = __esm({
|
|
12062
|
+
"src/core/doctor/format.ts"() {
|
|
12063
|
+
"use strict";
|
|
12064
|
+
}
|
|
12065
|
+
});
|
|
12066
|
+
|
|
11343
12067
|
// src/plugins/telegram/commands/doctor.ts
|
|
11344
12068
|
import { InlineKeyboard as InlineKeyboard8 } from "grammy";
|
|
12069
|
+
function escapeHtmlToLimit(text3, limit) {
|
|
12070
|
+
if (limit <= 0) return "";
|
|
12071
|
+
const chunks = [];
|
|
12072
|
+
let length = 0;
|
|
12073
|
+
let shortened = false;
|
|
12074
|
+
for (const character of text3) {
|
|
12075
|
+
const escaped = escapeHtml2(character);
|
|
12076
|
+
if (length + escaped.length > limit) {
|
|
12077
|
+
shortened = true;
|
|
12078
|
+
break;
|
|
12079
|
+
}
|
|
12080
|
+
chunks.push(escaped);
|
|
12081
|
+
length += escaped.length;
|
|
12082
|
+
}
|
|
12083
|
+
if (!shortened) return chunks.join("");
|
|
12084
|
+
while (chunks.length && length + 1 > limit) length -= chunks.pop().length;
|
|
12085
|
+
return limit > 0 ? `${chunks.join("")}\u2026` : "";
|
|
12086
|
+
}
|
|
12087
|
+
function renderTruncatedReport(report, header2) {
|
|
12088
|
+
const icons = { pass: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
|
|
12089
|
+
const footer = `
|
|
12090
|
+
|
|
12091
|
+
${SHORTENED_FOOTER}`;
|
|
12092
|
+
const budget = TELEGRAM_REPORT_LIMIT - footer.length;
|
|
12093
|
+
const lines = [...header2];
|
|
12094
|
+
let length = lines.join("\n").length;
|
|
12095
|
+
const append = (line) => {
|
|
12096
|
+
const added = (lines.length ? 1 : 0) + line.length;
|
|
12097
|
+
if (length + added > budget) return false;
|
|
12098
|
+
lines.push(line);
|
|
12099
|
+
length += added;
|
|
12100
|
+
return true;
|
|
12101
|
+
};
|
|
12102
|
+
outer: for (const category of report.categories.filter((item) => item.results.some((result) => result.status !== "pass"))) {
|
|
12103
|
+
const remainingForCategory = budget - length - 1;
|
|
12104
|
+
const categoryText = escapeHtmlToLimit(category.name, Math.max(0, remainingForCategory - 7));
|
|
12105
|
+
if (!categoryText || !append(`<b>${categoryText}</b>`)) break;
|
|
12106
|
+
for (const result of category.results.filter((item) => item.status !== "pass")) {
|
|
12107
|
+
const prefix = ` ${icons[result.status]} `;
|
|
12108
|
+
const remaining = budget - length - 1 - prefix.length;
|
|
12109
|
+
if (remaining <= 0) break outer;
|
|
12110
|
+
const message = escapeHtmlToLimit(result.message, remaining);
|
|
12111
|
+
if (!append(`${prefix}${message}`)) break outer;
|
|
12112
|
+
if (message !== escapeHtml2(result.message)) break outer;
|
|
12113
|
+
}
|
|
12114
|
+
if (!append("")) break;
|
|
12115
|
+
}
|
|
12116
|
+
return `${lines.join("\n")}${footer}`;
|
|
12117
|
+
}
|
|
11345
12118
|
function renderReport(report) {
|
|
11346
12119
|
const icons = { pass: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
|
|
11347
|
-
const
|
|
11348
|
-
|
|
11349
|
-
|
|
11350
|
-
|
|
12120
|
+
const { passed } = report.summary;
|
|
12121
|
+
const header2 = ["\u{1FA7A} <b>OpenACP Doctor</b>", `<b>${doctorHeadline(report.summary)}</b>`, doctorSummary(report.summary, " \xB7 "), ""];
|
|
12122
|
+
const lines = [...header2];
|
|
12123
|
+
for (const category of report.categories.filter((item) => item.results.some((result) => result.status !== "pass"))) {
|
|
12124
|
+
lines.push(`<b>${escapeHtml2(category.name)}</b>`);
|
|
12125
|
+
for (const result of category.results.filter((item) => item.status !== "pass")) {
|
|
11351
12126
|
lines.push(` ${icons[result.status]} ${escapeHtml2(result.message)}`);
|
|
11352
12127
|
}
|
|
11353
12128
|
lines.push("");
|
|
11354
12129
|
}
|
|
11355
|
-
|
|
11356
|
-
const
|
|
11357
|
-
lines.push(`<b>Result:</b> ${passed} passed, ${warnings} warnings, ${failed} failed${fixedStr}`);
|
|
11358
|
-
let keyboard;
|
|
12130
|
+
if (passed) lines.push(`\u2705 ${passed} passed`);
|
|
12131
|
+
const keyboard = new InlineKeyboard8();
|
|
11359
12132
|
if (report.pendingFixes.length > 0) {
|
|
11360
|
-
keyboard = new InlineKeyboard8();
|
|
11361
12133
|
for (let i = 0; i < report.pendingFixes.length; i++) {
|
|
11362
12134
|
const label = `\u{1F527} Fix: ${report.pendingFixes[i].message.slice(0, 30)}`;
|
|
11363
12135
|
keyboard.text(label, `m:doctor:fix:${i}`).row();
|
|
11364
12136
|
}
|
|
11365
12137
|
}
|
|
11366
|
-
|
|
12138
|
+
keyboard.text("Run again", "m:doctor").row();
|
|
12139
|
+
keyboard.text("Speech-to-text settings", settingsCommandCallback("/speech")).row();
|
|
12140
|
+
keyboard.text("Network proxy settings", settingsCommandCallback("/proxy"));
|
|
12141
|
+
const text3 = lines.join("\n");
|
|
12142
|
+
return { text: text3.length <= TELEGRAM_REPORT_LIMIT ? text3 : renderTruncatedReport(report, header2), keyboard };
|
|
11367
12143
|
}
|
|
11368
12144
|
function escapeHtml2(text3) {
|
|
11369
12145
|
return text3.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -11387,8 +12163,8 @@ async function handleDoctor(ctx, core) {
|
|
|
11387
12163
|
await ctx.api.editMessageText(
|
|
11388
12164
|
ctx.chat.id,
|
|
11389
12165
|
statusMsg.message_id,
|
|
11390
|
-
`\u274C
|
|
11391
|
-
{ parse_mode: "HTML" }
|
|
12166
|
+
`\u274C Diagnostics could not finish. Check the OpenACP logs, then run the checks again.`,
|
|
12167
|
+
{ parse_mode: "HTML", reply_markup: new InlineKeyboard8().text("Run again", "m:doctor") }
|
|
11392
12168
|
);
|
|
11393
12169
|
}
|
|
11394
12170
|
}
|
|
@@ -11443,14 +12219,18 @@ function setupDoctorCallbacks(bot, core) {
|
|
|
11443
12219
|
await handleDoctor(ctx, core);
|
|
11444
12220
|
});
|
|
11445
12221
|
}
|
|
11446
|
-
var log30, pendingFixesStore;
|
|
12222
|
+
var log30, pendingFixesStore, TELEGRAM_REPORT_LIMIT, SHORTENED_FOOTER;
|
|
11447
12223
|
var init_doctor2 = __esm({
|
|
11448
12224
|
"src/plugins/telegram/commands/doctor.ts"() {
|
|
11449
12225
|
"use strict";
|
|
11450
12226
|
init_doctor();
|
|
11451
12227
|
init_log();
|
|
12228
|
+
init_callback_navigation();
|
|
12229
|
+
init_format();
|
|
11452
12230
|
log30 = createChildLogger({ module: "telegram-cmd-doctor" });
|
|
11453
12231
|
pendingFixesStore = /* @__PURE__ */ new Map();
|
|
12232
|
+
TELEGRAM_REPORT_LIMIT = 3900;
|
|
12233
|
+
SHORTENED_FOOTER = "\u2026Output shortened. Run openacp doctor on the host for full details.";
|
|
11454
12234
|
}
|
|
11455
12235
|
});
|
|
11456
12236
|
|
|
@@ -11932,8 +12712,8 @@ var init_commands = __esm({
|
|
|
11932
12712
|
{ command: "handoff", description: "Continue this session in your terminal" },
|
|
11933
12713
|
{ command: "restart", description: "Restart OpenACP" },
|
|
11934
12714
|
{ command: "update", description: "Update to latest version and restart" },
|
|
11935
|
-
{ command: "doctor", description: "
|
|
11936
|
-
{ command: "proxy", description: "
|
|
12715
|
+
{ command: "doctor", description: "Check OpenACP health" },
|
|
12716
|
+
{ command: "proxy", description: "Configure network proxy" },
|
|
11937
12717
|
{ command: "retry", description: "Re-check setup prerequisites (use if bot is stuck on startup)" },
|
|
11938
12718
|
{ command: "tunnel", description: "Create/stop tunnel for a local port" },
|
|
11939
12719
|
{ command: "tunnels", description: "List active tunnels" },
|
|
@@ -12953,7 +13733,7 @@ var init_types = __esm({
|
|
|
12953
13733
|
});
|
|
12954
13734
|
|
|
12955
13735
|
// src/core/commands/proxy.ts
|
|
12956
|
-
import { randomUUID as randomUUID6 } from "crypto";
|
|
13736
|
+
import { createHash as createHash7, randomUUID as randomUUID6 } from "crypto";
|
|
12957
13737
|
function clearProxyDraftsForChannel(channelId) {
|
|
12958
13738
|
const prefix = `${channelId}:`;
|
|
12959
13739
|
for (const [id, draft] of drafts) if (draft.owner.startsWith(prefix)) drafts.delete(id);
|
|
@@ -12966,13 +13746,14 @@ var init_proxy2 = __esm({
|
|
|
12966
13746
|
init_types();
|
|
12967
13747
|
init_proxy_store();
|
|
12968
13748
|
init_proxy_service();
|
|
13749
|
+
init_proxy_labels();
|
|
12969
13750
|
DRAFT_TTL_MS = 10 * 6e4;
|
|
12970
13751
|
drafts = /* @__PURE__ */ new Map();
|
|
12971
13752
|
}
|
|
12972
13753
|
});
|
|
12973
13754
|
|
|
12974
13755
|
// src/plugins/telegram/command-sync.ts
|
|
12975
|
-
import { createHash as
|
|
13756
|
+
import { createHash as createHash8 } from "crypto";
|
|
12976
13757
|
function validDescription(description) {
|
|
12977
13758
|
if (typeof description !== "string") return false;
|
|
12978
13759
|
const trimmed = description.trim();
|
|
@@ -13041,7 +13822,7 @@ function localeLabel(locale) {
|
|
|
13041
13822
|
async function synchronizeTelegramCommands(api, chatId, desired, options) {
|
|
13042
13823
|
if (!/^\d{1,20}$/.test(options.botId)) throw new Error("Telegram bot identity is invalid");
|
|
13043
13824
|
assertTelegramCommandBoundary(desired);
|
|
13044
|
-
const chatKey =
|
|
13825
|
+
const chatKey = createHash8("sha256").update(String(chatId)).digest("hex").slice(0, 16);
|
|
13045
13826
|
const scopes = [
|
|
13046
13827
|
{ name: "default", scope: { type: "default" }, ledgerName: "default" },
|
|
13047
13828
|
{ name: "chat", scope: { type: "chat", chat_id: chatId }, ledgerName: `chat:${chatKey}` },
|
|
@@ -13940,12 +14721,12 @@ ${p}` : p;
|
|
|
13940
14721
|
return await fn();
|
|
13941
14722
|
} catch (err) {
|
|
13942
14723
|
if (attempt === maxRetries) throw err;
|
|
13943
|
-
const
|
|
14724
|
+
const delay2 = baseDelayMs * Math.pow(2, attempt - 1);
|
|
13944
14725
|
log38.warn(
|
|
13945
|
-
{ err, attempt, maxRetries, delayMs:
|
|
13946
|
-
`${label} failed, retrying in ${
|
|
14726
|
+
{ err, attempt, maxRetries, delayMs: delay2, operation: label },
|
|
14727
|
+
`${label} failed, retrying in ${delay2}ms`
|
|
13947
14728
|
);
|
|
13948
|
-
await new Promise((r) => setTimeout(r,
|
|
14729
|
+
await new Promise((r) => setTimeout(r, delay2));
|
|
13949
14730
|
}
|
|
13950
14731
|
}
|
|
13951
14732
|
throw new Error("unreachable");
|
|
@@ -14320,16 +15101,16 @@ System topics have been created. Tap${assistantTopicLink} to get started.`,
|
|
|
14320
15101
|
);
|
|
14321
15102
|
} catch (err) {
|
|
14322
15103
|
log38.error({ err }, "Failed to complete initialization after prerequisites met \u2014 will retry");
|
|
14323
|
-
const
|
|
15104
|
+
const delay3 = schedule[Math.min(attempt, schedule.length - 1)];
|
|
14324
15105
|
attempt++;
|
|
14325
|
-
this._prerequisiteWatcher = setTimeout(retry,
|
|
15106
|
+
this._prerequisiteWatcher = setTimeout(retry, delay3);
|
|
14326
15107
|
}
|
|
14327
15108
|
return;
|
|
14328
15109
|
}
|
|
14329
15110
|
log38.debug({ issues: result.issues }, "Prerequisites not yet met, retrying");
|
|
14330
|
-
const
|
|
15111
|
+
const delay2 = schedule[Math.min(attempt, schedule.length - 1)];
|
|
14331
15112
|
attempt++;
|
|
14332
|
-
this._prerequisiteWatcher = setTimeout(retry,
|
|
15113
|
+
this._prerequisiteWatcher = setTimeout(retry, delay2);
|
|
14333
15114
|
};
|
|
14334
15115
|
this._prerequisiteWatcher = setTimeout(retry, schedule[0]);
|
|
14335
15116
|
}
|
|
@@ -19098,12 +19879,12 @@ var SessionBridge = class {
|
|
|
19098
19879
|
break;
|
|
19099
19880
|
case "image_content": {
|
|
19100
19881
|
if (this.deps.fileService) {
|
|
19101
|
-
const
|
|
19882
|
+
const fs41 = this.deps.fileService;
|
|
19102
19883
|
const sid = this.session.id;
|
|
19103
19884
|
const { data, mimeType } = event;
|
|
19104
19885
|
const buffer = Buffer.from(data, "base64");
|
|
19105
|
-
const ext =
|
|
19106
|
-
|
|
19886
|
+
const ext = fs41.extensionFromMime(mimeType);
|
|
19887
|
+
fs41.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
|
|
19107
19888
|
this.sendMessage(sid, {
|
|
19108
19889
|
type: "attachment",
|
|
19109
19890
|
text: "",
|
|
@@ -19115,12 +19896,12 @@ var SessionBridge = class {
|
|
|
19115
19896
|
}
|
|
19116
19897
|
case "audio_content": {
|
|
19117
19898
|
if (this.deps.fileService) {
|
|
19118
|
-
const
|
|
19899
|
+
const fs41 = this.deps.fileService;
|
|
19119
19900
|
const sid = this.session.id;
|
|
19120
19901
|
const { data, mimeType } = event;
|
|
19121
19902
|
const buffer = Buffer.from(data, "base64");
|
|
19122
|
-
const ext =
|
|
19123
|
-
|
|
19903
|
+
const ext = fs41.extensionFromMime(mimeType);
|
|
19904
|
+
fs41.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
|
|
19124
19905
|
this.sendMessage(sid, {
|
|
19125
19906
|
type: "attachment",
|
|
19126
19907
|
text: "",
|
|
@@ -22931,36 +23712,36 @@ init_doctor();
|
|
|
22931
23712
|
init_config_registry();
|
|
22932
23713
|
|
|
22933
23714
|
// src/core/config/config-editor.ts
|
|
22934
|
-
import * as
|
|
23715
|
+
import * as path37 from "path";
|
|
22935
23716
|
import * as clack2 from "@clack/prompts";
|
|
22936
23717
|
|
|
22937
23718
|
// src/cli/autostart.ts
|
|
22938
23719
|
init_log();
|
|
22939
|
-
import { execFileSync as
|
|
22940
|
-
import * as
|
|
22941
|
-
import * as
|
|
23720
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
23721
|
+
import * as fs34 from "fs";
|
|
23722
|
+
import * as path33 from "path";
|
|
22942
23723
|
import * as os9 from "os";
|
|
22943
23724
|
var log20 = createChildLogger({ module: "autostart" });
|
|
22944
|
-
var LEGACY_LAUNCHD_PLIST_PATH =
|
|
22945
|
-
var LEGACY_SYSTEMD_SERVICE_PATH =
|
|
23725
|
+
var LEGACY_LAUNCHD_PLIST_PATH = path33.join(os9.homedir(), "Library", "LaunchAgents", "com.openacp.daemon.plist");
|
|
23726
|
+
var LEGACY_SYSTEMD_SERVICE_PATH = path33.join(os9.homedir(), ".config", "systemd", "user", "openacp.service");
|
|
22946
23727
|
function getLaunchdLabel(instanceId) {
|
|
22947
23728
|
return `com.openacp.daemon.${instanceId}`;
|
|
22948
23729
|
}
|
|
22949
23730
|
function getLaunchdPlistPath(instanceId) {
|
|
22950
|
-
return
|
|
23731
|
+
return path33.join(os9.homedir(), "Library", "LaunchAgents", `${getLaunchdLabel(instanceId)}.plist`);
|
|
22951
23732
|
}
|
|
22952
23733
|
function getSystemdServiceName(instanceId) {
|
|
22953
23734
|
return `openacp-${instanceId}`;
|
|
22954
23735
|
}
|
|
22955
23736
|
function getSystemdServicePath(instanceId) {
|
|
22956
|
-
return
|
|
23737
|
+
return path33.join(os9.homedir(), ".config", "systemd", "user", `${getSystemdServiceName(instanceId)}.service`);
|
|
22957
23738
|
}
|
|
22958
23739
|
function getAutoStartState(instanceId) {
|
|
22959
23740
|
if (process.platform === "linux") {
|
|
22960
|
-
const installed =
|
|
23741
|
+
const installed = fs34.existsSync(getSystemdServicePath(instanceId));
|
|
22961
23742
|
if (!installed) return { installed: false, manager: null, active: false };
|
|
22962
23743
|
try {
|
|
22963
|
-
const output =
|
|
23744
|
+
const output = execFileSync7("systemctl", [
|
|
22964
23745
|
"--user",
|
|
22965
23746
|
"show",
|
|
22966
23747
|
getSystemdServiceName(instanceId),
|
|
@@ -22979,11 +23760,11 @@ function getAutoStartState(instanceId) {
|
|
|
22979
23760
|
}
|
|
22980
23761
|
}
|
|
22981
23762
|
if (process.platform === "darwin") {
|
|
22982
|
-
const installed =
|
|
23763
|
+
const installed = fs34.existsSync(getLaunchdPlistPath(instanceId));
|
|
22983
23764
|
if (!installed) return { installed: false, manager: null, active: false };
|
|
22984
23765
|
try {
|
|
22985
23766
|
const uid = process.getuid();
|
|
22986
|
-
const output =
|
|
23767
|
+
const output = execFileSync7("launchctl", ["print", `gui/${uid}/${getLaunchdLabel(instanceId)}`], {
|
|
22987
23768
|
encoding: "utf8",
|
|
22988
23769
|
stdio: ["ignore", "pipe", "pipe"]
|
|
22989
23770
|
});
|
|
@@ -23009,7 +23790,7 @@ function escapeSystemdValue(str) {
|
|
|
23009
23790
|
}
|
|
23010
23791
|
function generateLaunchdPlist(nodePath, cliPath, logDir2, instanceRoot, instanceId) {
|
|
23011
23792
|
const label = getLaunchdLabel(instanceId);
|
|
23012
|
-
const logFile =
|
|
23793
|
+
const logFile = path33.join(logDir2, "openacp.log");
|
|
23013
23794
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
23014
23795
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
23015
23796
|
<plist version="1.0">
|
|
@@ -23061,29 +23842,29 @@ WantedBy=default.target
|
|
|
23061
23842
|
`;
|
|
23062
23843
|
}
|
|
23063
23844
|
function migrateLegacy() {
|
|
23064
|
-
if (process.platform === "darwin" &&
|
|
23845
|
+
if (process.platform === "darwin" && fs34.existsSync(LEGACY_LAUNCHD_PLIST_PATH)) {
|
|
23065
23846
|
try {
|
|
23066
23847
|
const uid = process.getuid();
|
|
23067
|
-
|
|
23848
|
+
execFileSync7("launchctl", ["bootout", `gui/${uid}`, "com.openacp.daemon"], { stdio: "pipe" });
|
|
23068
23849
|
} catch {
|
|
23069
23850
|
}
|
|
23070
23851
|
try {
|
|
23071
|
-
|
|
23852
|
+
fs34.unlinkSync(LEGACY_LAUNCHD_PLIST_PATH);
|
|
23072
23853
|
} catch {
|
|
23073
23854
|
}
|
|
23074
23855
|
log20.info("Removed legacy single-instance LaunchAgent");
|
|
23075
23856
|
}
|
|
23076
|
-
if (process.platform === "linux" &&
|
|
23857
|
+
if (process.platform === "linux" && fs34.existsSync(LEGACY_SYSTEMD_SERVICE_PATH)) {
|
|
23077
23858
|
try {
|
|
23078
|
-
|
|
23859
|
+
execFileSync7("systemctl", ["--user", "disable", "openacp"], { stdio: "pipe" });
|
|
23079
23860
|
} catch {
|
|
23080
23861
|
}
|
|
23081
23862
|
try {
|
|
23082
|
-
|
|
23863
|
+
fs34.unlinkSync(LEGACY_SYSTEMD_SERVICE_PATH);
|
|
23083
23864
|
} catch {
|
|
23084
23865
|
}
|
|
23085
23866
|
try {
|
|
23086
|
-
|
|
23867
|
+
execFileSync7("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
|
|
23087
23868
|
} catch {
|
|
23088
23869
|
}
|
|
23089
23870
|
log20.info("Removed legacy single-instance systemd service");
|
|
@@ -23094,50 +23875,50 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
|
|
|
23094
23875
|
return { success: false, error: "Auto-start not supported on this platform" };
|
|
23095
23876
|
}
|
|
23096
23877
|
const nodePath = process.execPath;
|
|
23097
|
-
const cliPath =
|
|
23098
|
-
const resolvedLogDir = logDir2.startsWith("~") ?
|
|
23878
|
+
const cliPath = path33.resolve(process.argv[1]);
|
|
23879
|
+
const resolvedLogDir = logDir2.startsWith("~") ? path33.join(os9.homedir(), logDir2.slice(1)) : logDir2;
|
|
23099
23880
|
try {
|
|
23100
23881
|
migrateLegacy();
|
|
23101
23882
|
if (process.platform === "darwin") {
|
|
23102
23883
|
const plistPath = getLaunchdPlistPath(instanceId);
|
|
23103
23884
|
const plist = generateLaunchdPlist(nodePath, cliPath, resolvedLogDir, instanceRoot, instanceId);
|
|
23104
|
-
const dir =
|
|
23105
|
-
|
|
23885
|
+
const dir = path33.dirname(plistPath);
|
|
23886
|
+
fs34.mkdirSync(dir, { recursive: true });
|
|
23106
23887
|
const uid = process.getuid();
|
|
23107
23888
|
const domain = `gui/${uid}`;
|
|
23108
|
-
const previous =
|
|
23889
|
+
const previous = fs34.existsSync(plistPath) ? fs34.readFileSync(plistPath, "utf8") : void 0;
|
|
23109
23890
|
const previousState = getAutoStartState(instanceId);
|
|
23110
23891
|
const tmp = `${plistPath}.${process.pid}.tmp`;
|
|
23111
23892
|
let replaced = false;
|
|
23112
23893
|
let bootedOut = false;
|
|
23113
23894
|
try {
|
|
23114
|
-
|
|
23115
|
-
|
|
23116
|
-
|
|
23895
|
+
fs34.writeFileSync(tmp, plist, { mode: 384 });
|
|
23896
|
+
execFileSync7("plutil", ["-lint", tmp], { stdio: "pipe" });
|
|
23897
|
+
fs34.renameSync(tmp, plistPath);
|
|
23117
23898
|
replaced = true;
|
|
23118
23899
|
if (previousState.active) {
|
|
23119
|
-
|
|
23900
|
+
execFileSync7("launchctl", ["bootout", domain, plistPath], { stdio: "pipe" });
|
|
23120
23901
|
bootedOut = true;
|
|
23121
23902
|
}
|
|
23122
|
-
|
|
23903
|
+
execFileSync7("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
|
|
23123
23904
|
} catch (error) {
|
|
23124
23905
|
try {
|
|
23125
|
-
|
|
23906
|
+
fs34.rmSync(tmp, { force: true });
|
|
23126
23907
|
} catch {
|
|
23127
23908
|
}
|
|
23128
23909
|
if (replaced) {
|
|
23129
23910
|
let rollbackBootedOut = false;
|
|
23130
23911
|
try {
|
|
23131
|
-
|
|
23912
|
+
execFileSync7("launchctl", ["bootout", domain, plistPath], { stdio: "pipe" });
|
|
23132
23913
|
rollbackBootedOut = true;
|
|
23133
23914
|
} catch {
|
|
23134
23915
|
}
|
|
23135
|
-
if (previous === void 0)
|
|
23916
|
+
if (previous === void 0) fs34.rmSync(plistPath, { force: true });
|
|
23136
23917
|
else {
|
|
23137
23918
|
const rollbackTmp = `${plistPath}.${process.pid}.rollback.tmp`;
|
|
23138
|
-
|
|
23139
|
-
|
|
23140
|
-
if (previousState.active || bootedOut || rollbackBootedOut)
|
|
23919
|
+
fs34.writeFileSync(rollbackTmp, previous, { mode: 384 });
|
|
23920
|
+
fs34.renameSync(rollbackTmp, plistPath);
|
|
23921
|
+
if (previousState.active || bootedOut || rollbackBootedOut) execFileSync7("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
|
|
23141
23922
|
}
|
|
23142
23923
|
}
|
|
23143
23924
|
throw error;
|
|
@@ -23149,20 +23930,20 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
|
|
|
23149
23930
|
const servicePath = getSystemdServicePath(instanceId);
|
|
23150
23931
|
const serviceName = getSystemdServiceName(instanceId);
|
|
23151
23932
|
const unit = generateSystemdUnit(nodePath, cliPath, instanceRoot, instanceId);
|
|
23152
|
-
const dir =
|
|
23153
|
-
|
|
23154
|
-
const previous =
|
|
23933
|
+
const dir = path33.dirname(servicePath);
|
|
23934
|
+
fs34.mkdirSync(dir, { recursive: true });
|
|
23935
|
+
const previous = fs34.existsSync(servicePath) ? fs34.readFileSync(servicePath, "utf8") : void 0;
|
|
23155
23936
|
const tmp = `${servicePath}.${process.pid}.tmp`;
|
|
23156
|
-
|
|
23157
|
-
|
|
23937
|
+
fs34.writeFileSync(tmp, unit, { mode: 384 });
|
|
23938
|
+
fs34.renameSync(tmp, servicePath);
|
|
23158
23939
|
try {
|
|
23159
|
-
|
|
23160
|
-
|
|
23940
|
+
execFileSync7("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
|
|
23941
|
+
execFileSync7("systemctl", ["--user", "enable", serviceName], { stdio: "pipe" });
|
|
23161
23942
|
} catch (error) {
|
|
23162
|
-
if (previous === void 0)
|
|
23163
|
-
else
|
|
23943
|
+
if (previous === void 0) fs34.unlinkSync(servicePath);
|
|
23944
|
+
else fs34.writeFileSync(servicePath, previous);
|
|
23164
23945
|
try {
|
|
23165
|
-
|
|
23946
|
+
execFileSync7("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
|
|
23166
23947
|
} catch {
|
|
23167
23948
|
}
|
|
23168
23949
|
throw error;
|
|
@@ -23184,13 +23965,13 @@ function uninstallAutoStart(instanceId) {
|
|
|
23184
23965
|
try {
|
|
23185
23966
|
if (process.platform === "darwin") {
|
|
23186
23967
|
const plistPath = getLaunchdPlistPath(instanceId);
|
|
23187
|
-
if (
|
|
23968
|
+
if (fs34.existsSync(plistPath)) {
|
|
23188
23969
|
const uid = process.getuid();
|
|
23189
23970
|
try {
|
|
23190
|
-
|
|
23971
|
+
execFileSync7("launchctl", ["bootout", `gui/${uid}`, plistPath], { stdio: "pipe" });
|
|
23191
23972
|
} catch {
|
|
23192
23973
|
}
|
|
23193
|
-
|
|
23974
|
+
fs34.unlinkSync(plistPath);
|
|
23194
23975
|
log20.info({ instanceId }, "LaunchAgent removed");
|
|
23195
23976
|
}
|
|
23196
23977
|
return { success: true };
|
|
@@ -23198,13 +23979,13 @@ function uninstallAutoStart(instanceId) {
|
|
|
23198
23979
|
if (process.platform === "linux") {
|
|
23199
23980
|
const servicePath = getSystemdServicePath(instanceId);
|
|
23200
23981
|
const serviceName = getSystemdServiceName(instanceId);
|
|
23201
|
-
if (
|
|
23982
|
+
if (fs34.existsSync(servicePath)) {
|
|
23202
23983
|
try {
|
|
23203
|
-
|
|
23984
|
+
execFileSync7("systemctl", ["--user", "disable", serviceName], { stdio: "pipe" });
|
|
23204
23985
|
} catch {
|
|
23205
23986
|
}
|
|
23206
|
-
|
|
23207
|
-
|
|
23987
|
+
fs34.unlinkSync(servicePath);
|
|
23988
|
+
execFileSync7("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
|
|
23208
23989
|
log20.info({ instanceId }, "systemd user service removed");
|
|
23209
23990
|
}
|
|
23210
23991
|
return { success: true };
|
|
@@ -23218,10 +23999,10 @@ function uninstallAutoStart(instanceId) {
|
|
|
23218
23999
|
}
|
|
23219
24000
|
function isAutoStartInstalled(instanceId) {
|
|
23220
24001
|
if (process.platform === "darwin") {
|
|
23221
|
-
return
|
|
24002
|
+
return fs34.existsSync(getLaunchdPlistPath(instanceId));
|
|
23222
24003
|
}
|
|
23223
24004
|
if (process.platform === "linux") {
|
|
23224
|
-
return
|
|
24005
|
+
return fs34.existsSync(getSystemdServicePath(instanceId));
|
|
23225
24006
|
}
|
|
23226
24007
|
return false;
|
|
23227
24008
|
}
|
|
@@ -23230,25 +24011,25 @@ function isAutoStartInstalled(instanceId) {
|
|
|
23230
24011
|
init_instance_context();
|
|
23231
24012
|
init_instance_registry();
|
|
23232
24013
|
init_log();
|
|
23233
|
-
import
|
|
23234
|
-
import
|
|
24014
|
+
import fs35 from "fs";
|
|
24015
|
+
import path34 from "path";
|
|
23235
24016
|
var log21 = createChildLogger({ module: "resolve-instance-id" });
|
|
23236
24017
|
function resolveInstanceId(instanceRoot) {
|
|
23237
24018
|
try {
|
|
23238
|
-
const configPath =
|
|
23239
|
-
const raw = JSON.parse(
|
|
24019
|
+
const configPath = path34.join(instanceRoot, "config.json");
|
|
24020
|
+
const raw = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
|
|
23240
24021
|
if (raw.id && typeof raw.id === "string") return raw.id;
|
|
23241
24022
|
} catch {
|
|
23242
24023
|
}
|
|
23243
24024
|
try {
|
|
23244
|
-
const reg = new InstanceRegistry(
|
|
24025
|
+
const reg = new InstanceRegistry(path34.join(getGlobalRoot(), "instances.json"));
|
|
23245
24026
|
reg.load();
|
|
23246
24027
|
const entry = reg.getByRoot(instanceRoot);
|
|
23247
24028
|
if (entry?.id) return entry.id;
|
|
23248
24029
|
} catch (err) {
|
|
23249
24030
|
log21.debug({ err: err.message, instanceRoot }, "Could not read instance registry, using fallback id");
|
|
23250
24031
|
}
|
|
23251
|
-
return
|
|
24032
|
+
return path34.basename(path34.dirname(instanceRoot)).replace(/[^a-zA-Z0-9-]/g, "-") || "default";
|
|
23252
24033
|
}
|
|
23253
24034
|
|
|
23254
24035
|
// src/core/config/config-editor.ts
|
|
@@ -23820,7 +24601,7 @@ async function runConfigEditor(configManager, mode = "file", apiPort, settingsMa
|
|
|
23820
24601
|
await configManager.load();
|
|
23821
24602
|
const config = configManager.get();
|
|
23822
24603
|
const updates = {};
|
|
23823
|
-
const instanceRoot =
|
|
24604
|
+
const instanceRoot = path37.dirname(configManager.getConfigPath());
|
|
23824
24605
|
console.log(`
|
|
23825
24606
|
${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
|
|
23826
24607
|
console.log(dim(`Config: ${configManager.getConfigPath()}`));
|
|
@@ -23877,17 +24658,17 @@ ${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
|
|
|
23877
24658
|
async function sendConfigViaApi(port, updates) {
|
|
23878
24659
|
const { apiCall: call } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
|
|
23879
24660
|
const paths = flattenToPaths(updates);
|
|
23880
|
-
for (const { path:
|
|
24661
|
+
for (const { path: path42, value } of paths) {
|
|
23881
24662
|
const res = await call(port, "/api/config", {
|
|
23882
24663
|
method: "PATCH",
|
|
23883
24664
|
headers: { "Content-Type": "application/json" },
|
|
23884
|
-
body: JSON.stringify({ path:
|
|
24665
|
+
body: JSON.stringify({ path: path42, value })
|
|
23885
24666
|
});
|
|
23886
24667
|
const data = await res.json();
|
|
23887
24668
|
if (!res.ok) {
|
|
23888
|
-
console.log(warn(`Failed to update ${
|
|
24669
|
+
console.log(warn(`Failed to update ${path42}: ${data.error}`));
|
|
23889
24670
|
} else if (data.needsRestart) {
|
|
23890
|
-
console.log(warn(`${
|
|
24671
|
+
console.log(warn(`${path42} updated \u2014 restart required`));
|
|
23891
24672
|
}
|
|
23892
24673
|
}
|
|
23893
24674
|
}
|
|
@@ -23906,26 +24687,28 @@ function flattenToPaths(obj, prefix = "") {
|
|
|
23906
24687
|
|
|
23907
24688
|
// src/cli/daemon.ts
|
|
23908
24689
|
init_config();
|
|
24690
|
+
init_daemon_identity();
|
|
23909
24691
|
import { spawn as spawn3 } from "child_process";
|
|
23910
|
-
import * as
|
|
23911
|
-
import * as
|
|
24692
|
+
import * as fs37 from "fs";
|
|
24693
|
+
import * as path38 from "path";
|
|
23912
24694
|
function getPidPath(root) {
|
|
23913
|
-
return
|
|
24695
|
+
return path38.join(root, "openacp.pid");
|
|
23914
24696
|
}
|
|
23915
24697
|
function getLogDir(root) {
|
|
23916
|
-
return
|
|
24698
|
+
return path38.join(root, "logs");
|
|
23917
24699
|
}
|
|
23918
24700
|
function getRunningMarker(root) {
|
|
23919
|
-
return
|
|
24701
|
+
return path38.join(root, "running");
|
|
23920
24702
|
}
|
|
23921
24703
|
function writePidFile(pidPath, pid) {
|
|
23922
|
-
const dir =
|
|
23923
|
-
|
|
23924
|
-
|
|
24704
|
+
const dir = path38.dirname(pidPath);
|
|
24705
|
+
fs37.mkdirSync(dir, { recursive: true });
|
|
24706
|
+
fs37.writeFileSync(pidPath, String(pid));
|
|
24707
|
+
writeDaemonIdentity(pidPath, pid);
|
|
23925
24708
|
}
|
|
23926
24709
|
function readPidFile(pidPath) {
|
|
23927
24710
|
try {
|
|
23928
|
-
const content =
|
|
24711
|
+
const content = fs37.readFileSync(pidPath, "utf-8").trim();
|
|
23929
24712
|
const pid = parseInt(content, 10);
|
|
23930
24713
|
return isNaN(pid) ? null : pid;
|
|
23931
24714
|
} catch {
|
|
@@ -23934,9 +24717,10 @@ function readPidFile(pidPath) {
|
|
|
23934
24717
|
}
|
|
23935
24718
|
function removePidFile(pidPath) {
|
|
23936
24719
|
try {
|
|
23937
|
-
|
|
24720
|
+
fs37.unlinkSync(pidPath);
|
|
23938
24721
|
} catch {
|
|
23939
24722
|
}
|
|
24723
|
+
removeDaemonIdentity(pidPath);
|
|
23940
24724
|
}
|
|
23941
24725
|
function isProcessRunning(pidPath) {
|
|
23942
24726
|
const pid = readPidFile(pidPath);
|
|
@@ -23967,12 +24751,12 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
|
|
|
23967
24751
|
return { error: `Already running (PID ${pid})` };
|
|
23968
24752
|
}
|
|
23969
24753
|
const resolvedLogDir = logDir2 ? expandHome2(logDir2) : getLogDir(instanceRoot);
|
|
23970
|
-
|
|
23971
|
-
const logFile =
|
|
23972
|
-
const cliPath =
|
|
24754
|
+
fs37.mkdirSync(resolvedLogDir, { recursive: true });
|
|
24755
|
+
const logFile = path38.join(resolvedLogDir, "openacp.log");
|
|
24756
|
+
const cliPath = path38.resolve(process.argv[1]);
|
|
23973
24757
|
const nodePath = process.execPath;
|
|
23974
|
-
const out =
|
|
23975
|
-
const err =
|
|
24758
|
+
const out = fs37.openSync(logFile, "a");
|
|
24759
|
+
const err = fs37.openSync(logFile, "a");
|
|
23976
24760
|
const child = spawn3(nodePath, [cliPath, "--daemon-child"], {
|
|
23977
24761
|
detached: true,
|
|
23978
24762
|
stdio: ["ignore", out, err],
|
|
@@ -23981,8 +24765,8 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
|
|
|
23981
24765
|
...instanceRoot ? { OPENACP_INSTANCE_ROOT: instanceRoot } : {}
|
|
23982
24766
|
}
|
|
23983
24767
|
});
|
|
23984
|
-
|
|
23985
|
-
|
|
24768
|
+
fs37.closeSync(out);
|
|
24769
|
+
fs37.closeSync(err);
|
|
23986
24770
|
if (!child.pid) {
|
|
23987
24771
|
return { error: "Failed to spawn daemon process" };
|
|
23988
24772
|
}
|
|
@@ -23993,7 +24777,7 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
|
|
|
23993
24777
|
function sleep2(ms) {
|
|
23994
24778
|
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
23995
24779
|
}
|
|
23996
|
-
function
|
|
24780
|
+
function isProcessAlive3(pid) {
|
|
23997
24781
|
try {
|
|
23998
24782
|
process.kill(pid, 0);
|
|
23999
24783
|
return "alive";
|
|
@@ -24006,7 +24790,7 @@ function isProcessAlive2(pid) {
|
|
|
24006
24790
|
async function stopDaemon(pidPath, instanceRoot) {
|
|
24007
24791
|
const pid = readPidFile(pidPath);
|
|
24008
24792
|
if (pid === null) return { stopped: false, error: "Not running (no PID file)" };
|
|
24009
|
-
const status =
|
|
24793
|
+
const status = isProcessAlive3(pid);
|
|
24010
24794
|
if (status === "dead") {
|
|
24011
24795
|
removePidFile(pidPath);
|
|
24012
24796
|
return { stopped: false, error: "Not running (stale PID file removed)" };
|
|
@@ -24026,7 +24810,7 @@ async function stopDaemon(pidPath, instanceRoot) {
|
|
|
24026
24810
|
const start = Date.now();
|
|
24027
24811
|
while (Date.now() - start < TIMEOUT) {
|
|
24028
24812
|
await sleep2(POLL_INTERVAL);
|
|
24029
|
-
const s =
|
|
24813
|
+
const s = isProcessAlive3(pid);
|
|
24030
24814
|
if (s === "dead" || s === "eperm") {
|
|
24031
24815
|
removePidFile(pidPath);
|
|
24032
24816
|
return { stopped: true, pid };
|
|
@@ -24043,7 +24827,7 @@ async function stopDaemon(pidPath, instanceRoot) {
|
|
|
24043
24827
|
const killStart = Date.now();
|
|
24044
24828
|
while (Date.now() - killStart < 1e3) {
|
|
24045
24829
|
await sleep2(POLL_INTERVAL);
|
|
24046
|
-
const s =
|
|
24830
|
+
const s = isProcessAlive3(pid);
|
|
24047
24831
|
if (s === "dead" || s === "eperm") {
|
|
24048
24832
|
removePidFile(pidPath);
|
|
24049
24833
|
return { stopped: true, pid };
|
|
@@ -24053,12 +24837,12 @@ async function stopDaemon(pidPath, instanceRoot) {
|
|
|
24053
24837
|
}
|
|
24054
24838
|
function markRunning(root) {
|
|
24055
24839
|
const marker = getRunningMarker(root);
|
|
24056
|
-
|
|
24057
|
-
|
|
24840
|
+
fs37.mkdirSync(path38.dirname(marker), { recursive: true });
|
|
24841
|
+
fs37.writeFileSync(marker, "");
|
|
24058
24842
|
}
|
|
24059
24843
|
function clearRunning(root) {
|
|
24060
24844
|
try {
|
|
24061
|
-
|
|
24845
|
+
fs37.unlinkSync(getRunningMarker(root));
|
|
24062
24846
|
} catch {
|
|
24063
24847
|
}
|
|
24064
24848
|
}
|