codeharbor 0.1.18 → 0.1.20
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/.env.example +5 -0
- package/README.md +19 -0
- package/dist/cli.js +335 -56
- package/package.json +3 -1
- package/scripts/postinstall-restart.cjs +134 -0
package/.env.example
CHANGED
|
@@ -63,6 +63,11 @@ CLI_COMPAT_PRESERVE_WHITESPACE=true
|
|
|
63
63
|
CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT=false
|
|
64
64
|
CLI_COMPAT_PROGRESS_THROTTLE_MS=300
|
|
65
65
|
CLI_COMPAT_FETCH_MEDIA=true
|
|
66
|
+
# Optional audio transcription for Matrix m.audio attachments.
|
|
67
|
+
CLI_COMPAT_TRANSCRIBE_AUDIO=false
|
|
68
|
+
CLI_COMPAT_AUDIO_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
|
|
69
|
+
CLI_COMPAT_AUDIO_TRANSCRIBE_TIMEOUT_MS=120000
|
|
70
|
+
CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS=6000
|
|
66
71
|
# Optional JSONL output path for executed prompt recording (for replay benchmarking).
|
|
67
72
|
CLI_COMPAT_RECORD_PATH=
|
|
68
73
|
|
package/README.md
CHANGED
|
@@ -93,6 +93,8 @@ Notes:
|
|
|
93
93
|
- Service commands auto-elevate with `sudo` when root privileges are required.
|
|
94
94
|
- `codeharbor service install --with-admin` and `install-linux-easy.sh --enable-admin-service` now install
|
|
95
95
|
`/etc/sudoers.d/codeharbor-restart` for non-root service users, so Admin UI restart actions work out-of-box.
|
|
96
|
+
- `npm install -g codeharbor@latest` now performs best-effort restart for active `codeharbor(.service)` units on Linux
|
|
97
|
+
so upgrades take effect immediately (set `CODEHARBOR_SKIP_POSTINSTALL_RESTART=1` to disable).
|
|
96
98
|
- If your environment blocks interactive `sudo`, use explicit fallback:
|
|
97
99
|
- `sudo <node-bin> <codeharbor-cli-script> service ...`
|
|
98
100
|
|
|
@@ -458,6 +460,14 @@ To make IM behavior closer to local `codex` CLI interaction, enable:
|
|
|
458
460
|
- lower update throttle for near-real-time progress
|
|
459
461
|
- `CLI_COMPAT_FETCH_MEDIA=true|false`
|
|
460
462
|
- download Matrix `mxc://` media (image) to temp file and pass it to codex via `--image`
|
|
463
|
+
- `CLI_COMPAT_TRANSCRIBE_AUDIO=true|false`
|
|
464
|
+
- download Matrix `m.audio` attachments and transcribe them into prompt context
|
|
465
|
+
- `CLI_COMPAT_AUDIO_TRANSCRIBE_MODEL`
|
|
466
|
+
- OpenAI transcription model (default `gpt-4o-mini-transcribe`)
|
|
467
|
+
- `CLI_COMPAT_AUDIO_TRANSCRIBE_TIMEOUT_MS`
|
|
468
|
+
- timeout for each audio transcription request
|
|
469
|
+
- `CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS`
|
|
470
|
+
- max transcript length appended to prompt for one attachment
|
|
461
471
|
- `CLI_COMPAT_RECORD_PATH=/abs/path/records.jsonl`
|
|
462
472
|
- append executed prompts as JSONL for replay benchmarking
|
|
463
473
|
|
|
@@ -501,6 +511,15 @@ When image attachments are present and `CLI_COMPAT_FETCH_MEDIA=true`, CodeHarbor
|
|
|
501
511
|
3. best-effort cleanup temp files after the request
|
|
502
512
|
4. optional prompt record append (`CLI_COMPAT_RECORD_PATH`) for deterministic replay input
|
|
503
513
|
|
|
514
|
+
When audio attachments are present and both `CLI_COMPAT_FETCH_MEDIA=true` and `CLI_COMPAT_TRANSCRIBE_AUDIO=true`, CodeHarbor will:
|
|
515
|
+
|
|
516
|
+
1. download `m.audio` media to a temp file
|
|
517
|
+
2. call OpenAI audio transcription API and append transcript to `[audio_transcripts]` prompt block
|
|
518
|
+
3. continue request even if transcription fails (warn log + no transcript)
|
|
519
|
+
4. best-effort cleanup temp files after the request
|
|
520
|
+
|
|
521
|
+
`OPENAI_API_KEY` is required only when audio transcription is enabled.
|
|
522
|
+
|
|
504
523
|
## Replay Benchmark
|
|
505
524
|
|
|
506
525
|
Replay recorded prompts directly against codex CLI to quantify drift and latency:
|
package/dist/cli.js
CHANGED
|
@@ -26,7 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
// src/cli.ts
|
|
27
27
|
var import_node_child_process7 = require("child_process");
|
|
28
28
|
var import_node_fs11 = __toESM(require("fs"));
|
|
29
|
-
var
|
|
29
|
+
var import_node_path15 = __toESM(require("path"));
|
|
30
30
|
var import_commander = require("commander");
|
|
31
31
|
|
|
32
32
|
// src/app.ts
|
|
@@ -353,6 +353,19 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
353
353
|
<input id="global-cli-throttle" type="number" min="0" />
|
|
354
354
|
</label>
|
|
355
355
|
<label class="checkbox"><input id="global-cli-fetch-media" type="checkbox" /><span>Fetch media attachments</span></label>
|
|
356
|
+
<label class="checkbox"><input id="global-cli-transcribe-audio" type="checkbox" /><span>Transcribe audio attachments</span></label>
|
|
357
|
+
<label class="field">
|
|
358
|
+
<span class="field-label">Audio transcribe model</span>
|
|
359
|
+
<input id="global-cli-audio-model" type="text" />
|
|
360
|
+
</label>
|
|
361
|
+
<label class="field">
|
|
362
|
+
<span class="field-label">Audio transcribe timeout (ms)</span>
|
|
363
|
+
<input id="global-cli-audio-timeout" type="number" min="1" />
|
|
364
|
+
</label>
|
|
365
|
+
<label class="field">
|
|
366
|
+
<span class="field-label">Audio transcript max chars</span>
|
|
367
|
+
<input id="global-cli-audio-max-chars" type="number" min="1" />
|
|
368
|
+
</label>
|
|
356
369
|
<label class="checkbox"><input id="global-agent-enabled" type="checkbox" /><span>Enable multi-agent workflow</span></label>
|
|
357
370
|
<label class="field">
|
|
358
371
|
<span class="field-label">Workflow auto-repair rounds</span>
|
|
@@ -687,6 +700,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
687
700
|
document.getElementById("global-cli-disable-split").checked = Boolean(cliCompat.disableReplyChunkSplit);
|
|
688
701
|
document.getElementById("global-cli-throttle").value = String(cliCompat.progressThrottleMs || 0);
|
|
689
702
|
document.getElementById("global-cli-fetch-media").checked = Boolean(cliCompat.fetchMedia);
|
|
703
|
+
document.getElementById("global-cli-transcribe-audio").checked = Boolean(cliCompat.transcribeAudio);
|
|
704
|
+
document.getElementById("global-cli-audio-model").value = cliCompat.audioTranscribeModel || "gpt-4o-mini-transcribe";
|
|
705
|
+
document.getElementById("global-cli-audio-timeout").value = String(cliCompat.audioTranscribeTimeoutMs || 120000);
|
|
706
|
+
document.getElementById("global-cli-audio-max-chars").value = String(cliCompat.audioTranscribeMaxChars || 6000);
|
|
690
707
|
document.getElementById("global-agent-enabled").checked = Boolean(agentWorkflow.enabled);
|
|
691
708
|
document.getElementById("global-agent-repair-rounds").value = String(
|
|
692
709
|
typeof agentWorkflow.autoRepairMaxRounds === "number" ? agentWorkflow.autoRepairMaxRounds : 1
|
|
@@ -728,7 +745,11 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
728
745
|
preserveWhitespace: asBool("global-cli-whitespace"),
|
|
729
746
|
disableReplyChunkSplit: asBool("global-cli-disable-split"),
|
|
730
747
|
progressThrottleMs: asNumber("global-cli-throttle", 300),
|
|
731
|
-
fetchMedia: asBool("global-cli-fetch-media")
|
|
748
|
+
fetchMedia: asBool("global-cli-fetch-media"),
|
|
749
|
+
transcribeAudio: asBool("global-cli-transcribe-audio"),
|
|
750
|
+
audioTranscribeModel: asText("global-cli-audio-model") || "gpt-4o-mini-transcribe",
|
|
751
|
+
audioTranscribeTimeoutMs: asNumber("global-cli-audio-timeout", 120000),
|
|
752
|
+
audioTranscribeMaxChars: asNumber("global-cli-audio-max-chars", 6000)
|
|
732
753
|
},
|
|
733
754
|
agentWorkflow: {
|
|
734
755
|
enabled: asBool("global-agent-enabled"),
|
|
@@ -1966,6 +1987,44 @@ var AdminServer = class {
|
|
|
1966
1987
|
envUpdates.CLI_COMPAT_FETCH_MEDIA = String(value);
|
|
1967
1988
|
updatedKeys.push("cliCompat.fetchMedia");
|
|
1968
1989
|
}
|
|
1990
|
+
if ("transcribeAudio" in compat) {
|
|
1991
|
+
const value = normalizeBoolean(compat.transcribeAudio, this.config.cliCompat.transcribeAudio);
|
|
1992
|
+
this.config.cliCompat.transcribeAudio = value;
|
|
1993
|
+
envUpdates.CLI_COMPAT_TRANSCRIBE_AUDIO = String(value);
|
|
1994
|
+
updatedKeys.push("cliCompat.transcribeAudio");
|
|
1995
|
+
}
|
|
1996
|
+
if ("audioTranscribeModel" in compat) {
|
|
1997
|
+
const value = normalizeString(
|
|
1998
|
+
compat.audioTranscribeModel,
|
|
1999
|
+
this.config.cliCompat.audioTranscribeModel,
|
|
2000
|
+
"cliCompat.audioTranscribeModel"
|
|
2001
|
+
);
|
|
2002
|
+
this.config.cliCompat.audioTranscribeModel = value || "gpt-4o-mini-transcribe";
|
|
2003
|
+
envUpdates.CLI_COMPAT_AUDIO_TRANSCRIBE_MODEL = this.config.cliCompat.audioTranscribeModel;
|
|
2004
|
+
updatedKeys.push("cliCompat.audioTranscribeModel");
|
|
2005
|
+
}
|
|
2006
|
+
if ("audioTranscribeTimeoutMs" in compat) {
|
|
2007
|
+
const value = normalizePositiveInt(
|
|
2008
|
+
compat.audioTranscribeTimeoutMs,
|
|
2009
|
+
this.config.cliCompat.audioTranscribeTimeoutMs,
|
|
2010
|
+
1,
|
|
2011
|
+
Number.MAX_SAFE_INTEGER
|
|
2012
|
+
);
|
|
2013
|
+
this.config.cliCompat.audioTranscribeTimeoutMs = value;
|
|
2014
|
+
envUpdates.CLI_COMPAT_AUDIO_TRANSCRIBE_TIMEOUT_MS = String(value);
|
|
2015
|
+
updatedKeys.push("cliCompat.audioTranscribeTimeoutMs");
|
|
2016
|
+
}
|
|
2017
|
+
if ("audioTranscribeMaxChars" in compat) {
|
|
2018
|
+
const value = normalizePositiveInt(
|
|
2019
|
+
compat.audioTranscribeMaxChars,
|
|
2020
|
+
this.config.cliCompat.audioTranscribeMaxChars,
|
|
2021
|
+
1,
|
|
2022
|
+
Number.MAX_SAFE_INTEGER
|
|
2023
|
+
);
|
|
2024
|
+
this.config.cliCompat.audioTranscribeMaxChars = value;
|
|
2025
|
+
envUpdates.CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS = String(value);
|
|
2026
|
+
updatedKeys.push("cliCompat.audioTranscribeMaxChars");
|
|
2027
|
+
}
|
|
1969
2028
|
}
|
|
1970
2029
|
if ("agentWorkflow" in body) {
|
|
1971
2030
|
const workflow = asObject(body.agentWorkflow, "agentWorkflow");
|
|
@@ -2621,6 +2680,7 @@ var MatrixChannel = class {
|
|
|
2621
2680
|
splitReplies;
|
|
2622
2681
|
preserveWhitespace;
|
|
2623
2682
|
fetchMedia;
|
|
2683
|
+
transcribeAudio;
|
|
2624
2684
|
client;
|
|
2625
2685
|
handler = null;
|
|
2626
2686
|
started = false;
|
|
@@ -2631,6 +2691,7 @@ var MatrixChannel = class {
|
|
|
2631
2691
|
this.splitReplies = !config.cliCompat.disableReplyChunkSplit;
|
|
2632
2692
|
this.preserveWhitespace = config.cliCompat.preserveWhitespace;
|
|
2633
2693
|
this.fetchMedia = config.cliCompat.fetchMedia;
|
|
2694
|
+
this.transcribeAudio = config.cliCompat.transcribeAudio;
|
|
2634
2695
|
this.client = (0, import_matrix_js_sdk.createClient)({
|
|
2635
2696
|
baseUrl: config.matrixHomeserver,
|
|
2636
2697
|
accessToken: config.matrixAccessToken,
|
|
@@ -2875,7 +2936,7 @@ var MatrixChannel = class {
|
|
|
2875
2936
|
}
|
|
2876
2937
|
const hydrated = await Promise.all(
|
|
2877
2938
|
attachments.map(async (attachment, index) => {
|
|
2878
|
-
if (attachment.kind
|
|
2939
|
+
if (!shouldHydrateAttachment(attachment.kind, this.transcribeAudio) || !attachment.mxcUrl) {
|
|
2879
2940
|
return attachment;
|
|
2880
2941
|
}
|
|
2881
2942
|
try {
|
|
@@ -3093,6 +3154,15 @@ function parseMxcUrl(mxcUrl) {
|
|
|
3093
3154
|
const mediaId = stripped.slice(slashIndex + 1);
|
|
3094
3155
|
return { serverName, mediaId };
|
|
3095
3156
|
}
|
|
3157
|
+
function shouldHydrateAttachment(kind, transcribeAudio) {
|
|
3158
|
+
if (kind === "image") {
|
|
3159
|
+
return true;
|
|
3160
|
+
}
|
|
3161
|
+
if (kind === "audio") {
|
|
3162
|
+
return transcribeAudio;
|
|
3163
|
+
}
|
|
3164
|
+
return false;
|
|
3165
|
+
}
|
|
3096
3166
|
function sanitizeFilename(value) {
|
|
3097
3167
|
return value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
|
|
3098
3168
|
}
|
|
@@ -3110,6 +3180,21 @@ function resolveFileExtension(fileName, mimeType) {
|
|
|
3110
3180
|
if (mimeType === "image/webp") {
|
|
3111
3181
|
return ".webp";
|
|
3112
3182
|
}
|
|
3183
|
+
if (mimeType === "audio/mpeg") {
|
|
3184
|
+
return ".mp3";
|
|
3185
|
+
}
|
|
3186
|
+
if (mimeType === "audio/mp4" || mimeType === "audio/x-m4a") {
|
|
3187
|
+
return ".m4a";
|
|
3188
|
+
}
|
|
3189
|
+
if (mimeType === "audio/wav" || mimeType === "audio/x-wav") {
|
|
3190
|
+
return ".wav";
|
|
3191
|
+
}
|
|
3192
|
+
if (mimeType === "audio/ogg") {
|
|
3193
|
+
return ".ogg";
|
|
3194
|
+
}
|
|
3195
|
+
if (mimeType === "audio/flac") {
|
|
3196
|
+
return ".flac";
|
|
3197
|
+
}
|
|
3113
3198
|
return ".bin";
|
|
3114
3199
|
}
|
|
3115
3200
|
function buildMatrixRichMessageContent(body, msgtype) {
|
|
@@ -3627,17 +3712,100 @@ function stringify(value) {
|
|
|
3627
3712
|
|
|
3628
3713
|
// src/orchestrator.ts
|
|
3629
3714
|
var import_async_mutex = require("async-mutex");
|
|
3630
|
-
var
|
|
3715
|
+
var import_promises5 = __toESM(require("fs/promises"));
|
|
3716
|
+
|
|
3717
|
+
// src/audio-transcriber.ts
|
|
3718
|
+
var import_promises3 = __toESM(require("fs/promises"));
|
|
3719
|
+
var import_node_path8 = __toESM(require("path"));
|
|
3720
|
+
var AudioTranscriber = class {
|
|
3721
|
+
enabled;
|
|
3722
|
+
apiKey;
|
|
3723
|
+
model;
|
|
3724
|
+
timeoutMs;
|
|
3725
|
+
maxChars;
|
|
3726
|
+
constructor(options) {
|
|
3727
|
+
this.enabled = options.enabled;
|
|
3728
|
+
this.apiKey = options.apiKey;
|
|
3729
|
+
this.model = options.model;
|
|
3730
|
+
this.timeoutMs = options.timeoutMs;
|
|
3731
|
+
this.maxChars = options.maxChars;
|
|
3732
|
+
}
|
|
3733
|
+
isEnabled() {
|
|
3734
|
+
return this.enabled;
|
|
3735
|
+
}
|
|
3736
|
+
async transcribeMany(attachments) {
|
|
3737
|
+
if (!this.enabled || attachments.length === 0) {
|
|
3738
|
+
return [];
|
|
3739
|
+
}
|
|
3740
|
+
if (!this.apiKey) {
|
|
3741
|
+
throw new Error(
|
|
3742
|
+
"Audio transcription is enabled but OPENAI_API_KEY is missing. Set OPENAI_API_KEY or disable CLI_COMPAT_TRANSCRIBE_AUDIO."
|
|
3743
|
+
);
|
|
3744
|
+
}
|
|
3745
|
+
const transcripts = [];
|
|
3746
|
+
for (const attachment of attachments) {
|
|
3747
|
+
const text = await this.transcribeOne(attachment);
|
|
3748
|
+
if (!text) {
|
|
3749
|
+
continue;
|
|
3750
|
+
}
|
|
3751
|
+
transcripts.push({
|
|
3752
|
+
name: attachment.name,
|
|
3753
|
+
text
|
|
3754
|
+
});
|
|
3755
|
+
}
|
|
3756
|
+
return transcripts;
|
|
3757
|
+
}
|
|
3758
|
+
async transcribeOne(attachment) {
|
|
3759
|
+
const buffer = await import_promises3.default.readFile(attachment.localPath);
|
|
3760
|
+
const formData = new FormData();
|
|
3761
|
+
formData.append("model", this.model);
|
|
3762
|
+
formData.append("response_format", "json");
|
|
3763
|
+
formData.append(
|
|
3764
|
+
"file",
|
|
3765
|
+
new Blob([buffer], { type: attachment.mimeType ?? "application/octet-stream" }),
|
|
3766
|
+
import_node_path8.default.basename(attachment.localPath)
|
|
3767
|
+
);
|
|
3768
|
+
const controller = new AbortController();
|
|
3769
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
3770
|
+
timer.unref?.();
|
|
3771
|
+
let response;
|
|
3772
|
+
try {
|
|
3773
|
+
response = await fetch("https://api.openai.com/v1/audio/transcriptions", {
|
|
3774
|
+
method: "POST",
|
|
3775
|
+
headers: {
|
|
3776
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
3777
|
+
},
|
|
3778
|
+
body: formData,
|
|
3779
|
+
signal: controller.signal
|
|
3780
|
+
});
|
|
3781
|
+
} finally {
|
|
3782
|
+
clearTimeout(timer);
|
|
3783
|
+
}
|
|
3784
|
+
const payload = await response.json().catch(() => ({}));
|
|
3785
|
+
if (!response.ok) {
|
|
3786
|
+
const message = typeof payload?.error?.message === "string" ? payload.error.message : `HTTP ${response.status} ${response.statusText}`;
|
|
3787
|
+
throw new Error(`Audio transcription failed for ${attachment.name}: ${message}`);
|
|
3788
|
+
}
|
|
3789
|
+
const text = typeof payload.text === "string" ? payload.text.trim() : "";
|
|
3790
|
+
if (!text) {
|
|
3791
|
+
return "";
|
|
3792
|
+
}
|
|
3793
|
+
if (text.length > this.maxChars) {
|
|
3794
|
+
return `${text.slice(0, this.maxChars)}...`;
|
|
3795
|
+
}
|
|
3796
|
+
return text;
|
|
3797
|
+
}
|
|
3798
|
+
};
|
|
3631
3799
|
|
|
3632
3800
|
// src/compat/cli-compat-recorder.ts
|
|
3633
3801
|
var import_node_fs6 = __toESM(require("fs"));
|
|
3634
|
-
var
|
|
3802
|
+
var import_node_path9 = __toESM(require("path"));
|
|
3635
3803
|
var CliCompatRecorder = class {
|
|
3636
3804
|
filePath;
|
|
3637
3805
|
chain = Promise.resolve();
|
|
3638
3806
|
constructor(filePath) {
|
|
3639
|
-
this.filePath =
|
|
3640
|
-
import_node_fs6.default.mkdirSync(
|
|
3807
|
+
this.filePath = import_node_path9.default.resolve(filePath);
|
|
3808
|
+
import_node_fs6.default.mkdirSync(import_node_path9.default.dirname(this.filePath), { recursive: true });
|
|
3641
3809
|
}
|
|
3642
3810
|
append(entry) {
|
|
3643
3811
|
const payload = `${JSON.stringify(entry)}
|
|
@@ -4122,8 +4290,8 @@ function createIdleWorkflowSnapshot() {
|
|
|
4122
4290
|
}
|
|
4123
4291
|
|
|
4124
4292
|
// src/workflow/autodev.ts
|
|
4125
|
-
var
|
|
4126
|
-
var
|
|
4293
|
+
var import_promises4 = __toESM(require("fs/promises"));
|
|
4294
|
+
var import_node_path10 = __toESM(require("path"));
|
|
4127
4295
|
function parseAutoDevCommand(text) {
|
|
4128
4296
|
const normalized = text.trim();
|
|
4129
4297
|
if (!/^\/autodev(?:\s|$)/i.test(normalized)) {
|
|
@@ -4143,8 +4311,8 @@ function parseAutoDevCommand(text) {
|
|
|
4143
4311
|
};
|
|
4144
4312
|
}
|
|
4145
4313
|
async function loadAutoDevContext(workdir) {
|
|
4146
|
-
const requirementsPath =
|
|
4147
|
-
const taskListPath =
|
|
4314
|
+
const requirementsPath = import_node_path10.default.join(workdir, "REQUIREMENTS.md");
|
|
4315
|
+
const taskListPath = import_node_path10.default.join(workdir, "TASK_LIST.md");
|
|
4148
4316
|
const requirementsContent = await readOptionalFile(requirementsPath);
|
|
4149
4317
|
const taskListContent = await readOptionalFile(taskListPath);
|
|
4150
4318
|
return {
|
|
@@ -4234,7 +4402,7 @@ function statusToSymbol(status) {
|
|
|
4234
4402
|
return "\u{1F6AB}";
|
|
4235
4403
|
}
|
|
4236
4404
|
async function updateAutoDevTaskStatus(taskListPath, task, nextStatus) {
|
|
4237
|
-
const content = await
|
|
4405
|
+
const content = await import_promises4.default.readFile(taskListPath, "utf8");
|
|
4238
4406
|
const lines = splitLines(content);
|
|
4239
4407
|
if (task.lineIndex < 0 || task.lineIndex >= lines.length) {
|
|
4240
4408
|
throw new Error(`task ${task.id} line index out of range`);
|
|
@@ -4244,7 +4412,7 @@ async function updateAutoDevTaskStatus(taskListPath, task, nextStatus) {
|
|
|
4244
4412
|
throw new Error(`failed to update task status for ${task.id}`);
|
|
4245
4413
|
}
|
|
4246
4414
|
lines[task.lineIndex] = updatedLine;
|
|
4247
|
-
await
|
|
4415
|
+
await import_promises4.default.writeFile(taskListPath, lines.join("\n"), "utf8");
|
|
4248
4416
|
return {
|
|
4249
4417
|
...task,
|
|
4250
4418
|
status: nextStatus
|
|
@@ -4252,7 +4420,7 @@ async function updateAutoDevTaskStatus(taskListPath, task, nextStatus) {
|
|
|
4252
4420
|
}
|
|
4253
4421
|
async function readOptionalFile(filePath) {
|
|
4254
4422
|
try {
|
|
4255
|
-
return await
|
|
4423
|
+
return await import_promises4.default.readFile(filePath, "utf8");
|
|
4256
4424
|
} catch (error) {
|
|
4257
4425
|
if (error.code === "ENOENT") {
|
|
4258
4426
|
return null;
|
|
@@ -4483,6 +4651,7 @@ var Orchestrator = class {
|
|
|
4483
4651
|
logger;
|
|
4484
4652
|
sessionLocks = /* @__PURE__ */ new Map();
|
|
4485
4653
|
runningExecutions = /* @__PURE__ */ new Map();
|
|
4654
|
+
pendingStopRequests = /* @__PURE__ */ new Set();
|
|
4486
4655
|
lockTtlMs;
|
|
4487
4656
|
lockPruneIntervalMs;
|
|
4488
4657
|
progressUpdatesEnabled;
|
|
@@ -4499,6 +4668,7 @@ var Orchestrator = class {
|
|
|
4499
4668
|
rateLimiter;
|
|
4500
4669
|
cliCompat;
|
|
4501
4670
|
cliCompatRecorder;
|
|
4671
|
+
audioTranscriber;
|
|
4502
4672
|
workflowRunner;
|
|
4503
4673
|
workflowSnapshots = /* @__PURE__ */ new Map();
|
|
4504
4674
|
autoDevSnapshots = /* @__PURE__ */ new Map();
|
|
@@ -4519,9 +4689,20 @@ var Orchestrator = class {
|
|
|
4519
4689
|
disableReplyChunkSplit: false,
|
|
4520
4690
|
progressThrottleMs: 300,
|
|
4521
4691
|
fetchMedia: false,
|
|
4692
|
+
transcribeAudio: false,
|
|
4693
|
+
audioTranscribeModel: "gpt-4o-mini-transcribe",
|
|
4694
|
+
audioTranscribeTimeoutMs: 12e4,
|
|
4695
|
+
audioTranscribeMaxChars: 6e3,
|
|
4522
4696
|
recordPath: null
|
|
4523
4697
|
};
|
|
4524
4698
|
this.cliCompatRecorder = this.cliCompat.recordPath ? new CliCompatRecorder(this.cliCompat.recordPath) : null;
|
|
4699
|
+
this.audioTranscriber = options?.audioTranscriber ?? new AudioTranscriber({
|
|
4700
|
+
enabled: this.cliCompat.transcribeAudio,
|
|
4701
|
+
apiKey: process.env.OPENAI_API_KEY?.trim() || null,
|
|
4702
|
+
model: this.cliCompat.audioTranscribeModel,
|
|
4703
|
+
timeoutMs: this.cliCompat.audioTranscribeTimeoutMs,
|
|
4704
|
+
maxChars: this.cliCompat.audioTranscribeMaxChars
|
|
4705
|
+
});
|
|
4525
4706
|
const defaultProgressInterval = options?.progressMinIntervalMs ?? 2500;
|
|
4526
4707
|
this.progressMinIntervalMs = this.cliCompat.enabled ? this.cliCompat.progressThrottleMs : defaultProgressInterval;
|
|
4527
4708
|
this.typingTimeoutMs = options?.typingTimeoutMs ?? 1e4;
|
|
@@ -4556,7 +4737,7 @@ var Orchestrator = class {
|
|
|
4556
4737
|
this.sessionRuntime = new CodexSessionRuntime(this.executor);
|
|
4557
4738
|
}
|
|
4558
4739
|
async handleMessage(message) {
|
|
4559
|
-
const attachmentPaths =
|
|
4740
|
+
const attachmentPaths = collectLocalAttachmentPaths(message);
|
|
4560
4741
|
try {
|
|
4561
4742
|
const receivedAt = Date.now();
|
|
4562
4743
|
const requestId = message.requestId || message.eventId;
|
|
@@ -4691,7 +4872,8 @@ var Orchestrator = class {
|
|
|
4691
4872
|
}
|
|
4692
4873
|
this.stateStore.activateSession(sessionKey, this.sessionActiveWindowMs);
|
|
4693
4874
|
const previousCodexSessionId = this.stateStore.getCodexSessionId(sessionKey);
|
|
4694
|
-
const
|
|
4875
|
+
const audioTranscripts = await this.transcribeAudioAttachments(message, requestId, sessionKey);
|
|
4876
|
+
const executionPrompt = this.buildExecutionPrompt(route.prompt, message, audioTranscripts);
|
|
4695
4877
|
const imagePaths = collectImagePaths(message);
|
|
4696
4878
|
let lastProgressAt = 0;
|
|
4697
4879
|
let lastProgressText = "";
|
|
@@ -4701,7 +4883,7 @@ var Orchestrator = class {
|
|
|
4701
4883
|
let executionDurationMs = 0;
|
|
4702
4884
|
let sendDurationMs = 0;
|
|
4703
4885
|
const requestStartedAt = Date.now();
|
|
4704
|
-
let cancelRequested =
|
|
4886
|
+
let cancelRequested = this.consumePendingStopRequest(sessionKey);
|
|
4705
4887
|
this.runningExecutions.set(sessionKey, {
|
|
4706
4888
|
requestId,
|
|
4707
4889
|
startedAt: requestStartedAt,
|
|
@@ -5208,6 +5390,7 @@ var Orchestrator = class {
|
|
|
5208
5390
|
this.sessionRuntime.clearSession(sessionKey);
|
|
5209
5391
|
const running = this.runningExecutions.get(sessionKey);
|
|
5210
5392
|
if (running) {
|
|
5393
|
+
this.pendingStopRequests.delete(sessionKey);
|
|
5211
5394
|
this.sessionRuntime.cancelRunningExecution(sessionKey);
|
|
5212
5395
|
running.cancel();
|
|
5213
5396
|
await this.channel.sendNotice(
|
|
@@ -5222,11 +5405,32 @@ var Orchestrator = class {
|
|
|
5222
5405
|
});
|
|
5223
5406
|
return;
|
|
5224
5407
|
}
|
|
5408
|
+
const lockEntry = this.sessionLocks.get(sessionKey);
|
|
5409
|
+
if (lockEntry?.mutex.isLocked()) {
|
|
5410
|
+
this.pendingStopRequests.add(sessionKey);
|
|
5411
|
+
await this.channel.sendNotice(
|
|
5412
|
+
message.conversationId,
|
|
5413
|
+
"[CodeHarbor] \u5DF2\u8BF7\u6C42\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\uFF0C\u5E76\u5DF2\u6E05\u7406\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002"
|
|
5414
|
+
);
|
|
5415
|
+
this.logger.info("Stop command queued for pending execution", {
|
|
5416
|
+
requestId,
|
|
5417
|
+
sessionKey
|
|
5418
|
+
});
|
|
5419
|
+
return;
|
|
5420
|
+
}
|
|
5421
|
+
this.pendingStopRequests.delete(sessionKey);
|
|
5225
5422
|
await this.channel.sendNotice(
|
|
5226
5423
|
message.conversationId,
|
|
5227
5424
|
"[CodeHarbor] \u4F1A\u8BDD\u5DF2\u505C\u6B62\u3002\u540E\u7EED\u5728\u7FA4\u804A\u4E2D\u8BF7\u63D0\u53CA/\u56DE\u590D\u6211\uFF0C\u6216\u5728\u79C1\u804A\u76F4\u63A5\u53D1\u9001\u6D88\u606F\u3002"
|
|
5228
5425
|
);
|
|
5229
5426
|
}
|
|
5427
|
+
consumePendingStopRequest(sessionKey) {
|
|
5428
|
+
if (!this.pendingStopRequests.has(sessionKey)) {
|
|
5429
|
+
return false;
|
|
5430
|
+
}
|
|
5431
|
+
this.pendingStopRequests.delete(sessionKey);
|
|
5432
|
+
return true;
|
|
5433
|
+
}
|
|
5230
5434
|
startTypingHeartbeat(conversationId) {
|
|
5231
5435
|
let stopped = false;
|
|
5232
5436
|
const refreshIntervalMs = Math.max(1e3, Math.floor(this.typingTimeoutMs / 2));
|
|
@@ -5328,8 +5532,41 @@ var Orchestrator = class {
|
|
|
5328
5532
|
}
|
|
5329
5533
|
return this.configService.resolveRoomConfig(conversationId, fallbackPolicy);
|
|
5330
5534
|
}
|
|
5331
|
-
|
|
5332
|
-
if (
|
|
5535
|
+
async transcribeAudioAttachments(message, requestId, sessionKey) {
|
|
5536
|
+
if (!this.audioTranscriber.isEnabled()) {
|
|
5537
|
+
return [];
|
|
5538
|
+
}
|
|
5539
|
+
const audioAttachments = message.attachments.filter((attachment) => attachment.kind === "audio" && Boolean(attachment.localPath)).map((attachment) => ({
|
|
5540
|
+
name: attachment.name,
|
|
5541
|
+
mimeType: attachment.mimeType,
|
|
5542
|
+
localPath: attachment.localPath
|
|
5543
|
+
}));
|
|
5544
|
+
if (audioAttachments.length === 0) {
|
|
5545
|
+
return [];
|
|
5546
|
+
}
|
|
5547
|
+
try {
|
|
5548
|
+
const transcripts = await this.audioTranscriber.transcribeMany(audioAttachments);
|
|
5549
|
+
if (transcripts.length > 0) {
|
|
5550
|
+
this.logger.info("Audio transcription completed", {
|
|
5551
|
+
requestId,
|
|
5552
|
+
sessionKey,
|
|
5553
|
+
attachmentCount: audioAttachments.length,
|
|
5554
|
+
transcriptCount: transcripts.length
|
|
5555
|
+
});
|
|
5556
|
+
}
|
|
5557
|
+
return transcripts;
|
|
5558
|
+
} catch (error) {
|
|
5559
|
+
this.logger.warn("Audio transcription failed, continuing without transcripts", {
|
|
5560
|
+
requestId,
|
|
5561
|
+
sessionKey,
|
|
5562
|
+
attachmentCount: audioAttachments.length,
|
|
5563
|
+
error: formatError3(error)
|
|
5564
|
+
});
|
|
5565
|
+
return [];
|
|
5566
|
+
}
|
|
5567
|
+
}
|
|
5568
|
+
buildExecutionPrompt(prompt, message, audioTranscripts) {
|
|
5569
|
+
if (message.attachments.length === 0 && audioTranscripts.length === 0) {
|
|
5333
5570
|
return prompt;
|
|
5334
5571
|
}
|
|
5335
5572
|
const attachmentSummary = message.attachments.map((attachment) => {
|
|
@@ -5340,11 +5577,19 @@ var Orchestrator = class {
|
|
|
5340
5577
|
return `- kind=${attachment.kind} name=${attachment.name} mime=${mime} size=${size} source=${source} local=${local}`;
|
|
5341
5578
|
}).join("\n");
|
|
5342
5579
|
const promptBody = prompt.trim() ? prompt : "(no text body)";
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
[attachments]
|
|
5580
|
+
const sections = [promptBody];
|
|
5581
|
+
if (attachmentSummary) {
|
|
5582
|
+
sections.push(`[attachments]
|
|
5346
5583
|
${attachmentSummary}
|
|
5347
|
-
[/attachments]
|
|
5584
|
+
[/attachments]`);
|
|
5585
|
+
}
|
|
5586
|
+
if (audioTranscripts.length > 0) {
|
|
5587
|
+
const transcriptSummary = audioTranscripts.map((transcript) => `- name=${transcript.name} text=${transcript.text.replace(/\s+/g, " ").trim()}`).join("\n");
|
|
5588
|
+
sections.push(`[audio_transcripts]
|
|
5589
|
+
${transcriptSummary}
|
|
5590
|
+
[/audio_transcripts]`);
|
|
5591
|
+
}
|
|
5592
|
+
return sections.join("\n\n");
|
|
5348
5593
|
}
|
|
5349
5594
|
async recordCliCompatPrompt(entry) {
|
|
5350
5595
|
if (!this.cliCompatRecorder) {
|
|
@@ -5489,11 +5734,21 @@ function collectImagePaths(message) {
|
|
|
5489
5734
|
}
|
|
5490
5735
|
return [...seen];
|
|
5491
5736
|
}
|
|
5492
|
-
|
|
5737
|
+
function collectLocalAttachmentPaths(message) {
|
|
5738
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5739
|
+
for (const attachment of message.attachments) {
|
|
5740
|
+
if (!attachment.localPath) {
|
|
5741
|
+
continue;
|
|
5742
|
+
}
|
|
5743
|
+
seen.add(attachment.localPath);
|
|
5744
|
+
}
|
|
5745
|
+
return [...seen];
|
|
5746
|
+
}
|
|
5747
|
+
async function cleanupAttachmentFiles(attachmentPaths) {
|
|
5493
5748
|
await Promise.all(
|
|
5494
|
-
|
|
5749
|
+
attachmentPaths.map(async (attachmentPath) => {
|
|
5495
5750
|
try {
|
|
5496
|
-
await
|
|
5751
|
+
await import_promises5.default.unlink(attachmentPath);
|
|
5497
5752
|
} catch {
|
|
5498
5753
|
}
|
|
5499
5754
|
})
|
|
@@ -5610,7 +5865,7 @@ ${result.review}
|
|
|
5610
5865
|
|
|
5611
5866
|
// src/store/state-store.ts
|
|
5612
5867
|
var import_node_fs7 = __toESM(require("fs"));
|
|
5613
|
-
var
|
|
5868
|
+
var import_node_path11 = __toESM(require("path"));
|
|
5614
5869
|
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
5615
5870
|
var PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
5616
5871
|
var SQLITE_MODULE_ID = `node:${"sqlite"}`;
|
|
@@ -5636,7 +5891,7 @@ var StateStore = class {
|
|
|
5636
5891
|
this.maxProcessedEventsPerSession = maxProcessedEventsPerSession;
|
|
5637
5892
|
this.maxSessionAgeMs = maxSessionAgeDays * ONE_DAY_MS;
|
|
5638
5893
|
this.maxSessions = maxSessions;
|
|
5639
|
-
import_node_fs7.default.mkdirSync(
|
|
5894
|
+
import_node_fs7.default.mkdirSync(import_node_path11.default.dirname(this.dbPath), { recursive: true });
|
|
5640
5895
|
this.db = new DatabaseSync(this.dbPath);
|
|
5641
5896
|
this.initializeSchema();
|
|
5642
5897
|
this.importLegacyStateIfNeeded();
|
|
@@ -6159,7 +6414,7 @@ function isNonLoopbackHost(host) {
|
|
|
6159
6414
|
|
|
6160
6415
|
// src/config.ts
|
|
6161
6416
|
var import_node_fs8 = __toESM(require("fs"));
|
|
6162
|
-
var
|
|
6417
|
+
var import_node_path12 = __toESM(require("path"));
|
|
6163
6418
|
var import_dotenv2 = __toESM(require("dotenv"));
|
|
6164
6419
|
var import_zod = require("zod");
|
|
6165
6420
|
var configSchema = import_zod.z.object({
|
|
@@ -6206,6 +6461,10 @@ var configSchema = import_zod.z.object({
|
|
|
6206
6461
|
CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
6207
6462
|
CLI_COMPAT_PROGRESS_THROTTLE_MS: import_zod.z.string().default("300").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
|
|
6208
6463
|
CLI_COMPAT_FETCH_MEDIA: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
6464
|
+
CLI_COMPAT_TRANSCRIBE_AUDIO: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
6465
|
+
CLI_COMPAT_AUDIO_TRANSCRIBE_MODEL: import_zod.z.string().default("gpt-4o-mini-transcribe"),
|
|
6466
|
+
CLI_COMPAT_AUDIO_TRANSCRIBE_TIMEOUT_MS: import_zod.z.string().default("120000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
6467
|
+
CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS: import_zod.z.string().default("6000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
6209
6468
|
CLI_COMPAT_RECORD_PATH: import_zod.z.string().default(""),
|
|
6210
6469
|
DOCTOR_HTTP_TIMEOUT_MS: import_zod.z.string().default("10000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
6211
6470
|
ADMIN_BIND_HOST: import_zod.z.string().default("127.0.0.1"),
|
|
@@ -6222,7 +6481,7 @@ var configSchema = import_zod.z.object({
|
|
|
6222
6481
|
matrixCommandPrefix: v.MATRIX_COMMAND_PREFIX,
|
|
6223
6482
|
codexBin: v.CODEX_BIN,
|
|
6224
6483
|
codexModel: v.CODEX_MODEL?.trim() || null,
|
|
6225
|
-
codexWorkdir:
|
|
6484
|
+
codexWorkdir: import_node_path12.default.resolve(v.CODEX_WORKDIR),
|
|
6226
6485
|
codexDangerousBypass: v.CODEX_DANGEROUS_BYPASS,
|
|
6227
6486
|
codexExecTimeoutMs: v.CODEX_EXEC_TIMEOUT_MS,
|
|
6228
6487
|
codexSandboxMode: v.CODEX_SANDBOX_MODE?.trim() || null,
|
|
@@ -6233,8 +6492,8 @@ var configSchema = import_zod.z.object({
|
|
|
6233
6492
|
enabled: v.AGENT_WORKFLOW_ENABLED,
|
|
6234
6493
|
autoRepairMaxRounds: v.AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS
|
|
6235
6494
|
},
|
|
6236
|
-
stateDbPath:
|
|
6237
|
-
legacyStateJsonPath: v.STATE_PATH.trim() ?
|
|
6495
|
+
stateDbPath: import_node_path12.default.resolve(v.STATE_DB_PATH),
|
|
6496
|
+
legacyStateJsonPath: v.STATE_PATH.trim() ? import_node_path12.default.resolve(v.STATE_PATH) : null,
|
|
6238
6497
|
maxProcessedEventsPerSession: v.MAX_PROCESSED_EVENTS_PER_SESSION,
|
|
6239
6498
|
maxSessionAgeDays: v.MAX_SESSION_AGE_DAYS,
|
|
6240
6499
|
maxSessions: v.MAX_SESSIONS,
|
|
@@ -6266,7 +6525,11 @@ var configSchema = import_zod.z.object({
|
|
|
6266
6525
|
disableReplyChunkSplit: v.CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT,
|
|
6267
6526
|
progressThrottleMs: v.CLI_COMPAT_PROGRESS_THROTTLE_MS,
|
|
6268
6527
|
fetchMedia: v.CLI_COMPAT_FETCH_MEDIA,
|
|
6269
|
-
|
|
6528
|
+
transcribeAudio: v.CLI_COMPAT_TRANSCRIBE_AUDIO,
|
|
6529
|
+
audioTranscribeModel: v.CLI_COMPAT_AUDIO_TRANSCRIBE_MODEL.trim() || "gpt-4o-mini-transcribe",
|
|
6530
|
+
audioTranscribeTimeoutMs: v.CLI_COMPAT_AUDIO_TRANSCRIBE_TIMEOUT_MS,
|
|
6531
|
+
audioTranscribeMaxChars: v.CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS,
|
|
6532
|
+
recordPath: v.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path12.default.resolve(v.CLI_COMPAT_RECORD_PATH) : null
|
|
6270
6533
|
},
|
|
6271
6534
|
doctorHttpTimeoutMs: v.DOCTOR_HTTP_TIMEOUT_MS,
|
|
6272
6535
|
adminBindHost: v.ADMIN_BIND_HOST.trim() || "127.0.0.1",
|
|
@@ -6277,7 +6540,7 @@ var configSchema = import_zod.z.object({
|
|
|
6277
6540
|
adminAllowedOrigins: parseCsvList(v.ADMIN_ALLOWED_ORIGINS),
|
|
6278
6541
|
logLevel: v.LOG_LEVEL
|
|
6279
6542
|
}));
|
|
6280
|
-
function loadEnvFromFile(filePath =
|
|
6543
|
+
function loadEnvFromFile(filePath = import_node_path12.default.resolve(process.cwd(), ".env"), env = process.env) {
|
|
6281
6544
|
import_dotenv2.default.config({
|
|
6282
6545
|
path: filePath,
|
|
6283
6546
|
processEnv: env,
|
|
@@ -6290,9 +6553,9 @@ function loadConfig(env = process.env) {
|
|
|
6290
6553
|
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`).join("; ");
|
|
6291
6554
|
throw new Error(`Invalid configuration: ${message}`);
|
|
6292
6555
|
}
|
|
6293
|
-
import_node_fs8.default.mkdirSync(
|
|
6556
|
+
import_node_fs8.default.mkdirSync(import_node_path12.default.dirname(parsed.data.stateDbPath), { recursive: true });
|
|
6294
6557
|
if (parsed.data.legacyStateJsonPath) {
|
|
6295
|
-
import_node_fs8.default.mkdirSync(
|
|
6558
|
+
import_node_fs8.default.mkdirSync(import_node_path12.default.dirname(parsed.data.legacyStateJsonPath), { recursive: true });
|
|
6296
6559
|
}
|
|
6297
6560
|
return parsed.data;
|
|
6298
6561
|
}
|
|
@@ -6460,7 +6723,7 @@ function parseAdminTokens(raw) {
|
|
|
6460
6723
|
|
|
6461
6724
|
// src/config-snapshot.ts
|
|
6462
6725
|
var import_node_fs9 = __toESM(require("fs"));
|
|
6463
|
-
var
|
|
6726
|
+
var import_node_path13 = __toESM(require("path"));
|
|
6464
6727
|
var import_zod2 = require("zod");
|
|
6465
6728
|
var CONFIG_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
6466
6729
|
var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
@@ -6507,6 +6770,10 @@ var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
|
6507
6770
|
"CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT",
|
|
6508
6771
|
"CLI_COMPAT_PROGRESS_THROTTLE_MS",
|
|
6509
6772
|
"CLI_COMPAT_FETCH_MEDIA",
|
|
6773
|
+
"CLI_COMPAT_TRANSCRIBE_AUDIO",
|
|
6774
|
+
"CLI_COMPAT_AUDIO_TRANSCRIBE_MODEL",
|
|
6775
|
+
"CLI_COMPAT_AUDIO_TRANSCRIBE_TIMEOUT_MS",
|
|
6776
|
+
"CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS",
|
|
6510
6777
|
"CLI_COMPAT_RECORD_PATH",
|
|
6511
6778
|
"DOCTOR_HTTP_TIMEOUT_MS",
|
|
6512
6779
|
"ADMIN_BIND_HOST",
|
|
@@ -6573,6 +6840,14 @@ var envSnapshotSchema = import_zod2.z.object({
|
|
|
6573
6840
|
CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT: booleanStringSchema("CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT"),
|
|
6574
6841
|
CLI_COMPAT_PROGRESS_THROTTLE_MS: integerStringSchema("CLI_COMPAT_PROGRESS_THROTTLE_MS", 0),
|
|
6575
6842
|
CLI_COMPAT_FETCH_MEDIA: booleanStringSchema("CLI_COMPAT_FETCH_MEDIA"),
|
|
6843
|
+
CLI_COMPAT_TRANSCRIBE_AUDIO: booleanStringSchema("CLI_COMPAT_TRANSCRIBE_AUDIO").default("false"),
|
|
6844
|
+
CLI_COMPAT_AUDIO_TRANSCRIBE_MODEL: import_zod2.z.string().default("gpt-4o-mini-transcribe"),
|
|
6845
|
+
CLI_COMPAT_AUDIO_TRANSCRIBE_TIMEOUT_MS: integerStringSchema("CLI_COMPAT_AUDIO_TRANSCRIBE_TIMEOUT_MS", 1).default(
|
|
6846
|
+
"120000"
|
|
6847
|
+
),
|
|
6848
|
+
CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS: integerStringSchema("CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS", 1).default(
|
|
6849
|
+
"6000"
|
|
6850
|
+
),
|
|
6576
6851
|
CLI_COMPAT_RECORD_PATH: import_zod2.z.string(),
|
|
6577
6852
|
DOCTOR_HTTP_TIMEOUT_MS: integerStringSchema("DOCTOR_HTTP_TIMEOUT_MS", 1),
|
|
6578
6853
|
ADMIN_BIND_HOST: import_zod2.z.string(),
|
|
@@ -6651,8 +6926,8 @@ async function runConfigExportCommand(options = {}) {
|
|
|
6651
6926
|
const snapshot = buildConfigSnapshot(config, stateStore.listRoomSettings(), options.now ?? /* @__PURE__ */ new Date());
|
|
6652
6927
|
const serialized = serializeConfigSnapshot(snapshot);
|
|
6653
6928
|
if (options.outputPath) {
|
|
6654
|
-
const targetPath =
|
|
6655
|
-
import_node_fs9.default.mkdirSync(
|
|
6929
|
+
const targetPath = import_node_path13.default.resolve(cwd, options.outputPath);
|
|
6930
|
+
import_node_fs9.default.mkdirSync(import_node_path13.default.dirname(targetPath), { recursive: true });
|
|
6656
6931
|
import_node_fs9.default.writeFileSync(targetPath, serialized, "utf8");
|
|
6657
6932
|
output.write(`Exported config snapshot to ${targetPath}
|
|
6658
6933
|
`);
|
|
@@ -6667,7 +6942,7 @@ async function runConfigImportCommand(options) {
|
|
|
6667
6942
|
const cwd = options.cwd ?? process.cwd();
|
|
6668
6943
|
const output = options.output ?? process.stdout;
|
|
6669
6944
|
const actor = options.actor?.trim() || "cli:config-import";
|
|
6670
|
-
const sourcePath =
|
|
6945
|
+
const sourcePath = import_node_path13.default.resolve(cwd, options.filePath);
|
|
6671
6946
|
if (!import_node_fs9.default.existsSync(sourcePath)) {
|
|
6672
6947
|
throw new Error(`Config snapshot file not found: ${sourcePath}`);
|
|
6673
6948
|
}
|
|
@@ -6698,7 +6973,7 @@ async function runConfigImportCommand(options) {
|
|
|
6698
6973
|
synchronizeRoomSettings(stateStore, normalizedRooms);
|
|
6699
6974
|
stateStore.appendConfigRevision(
|
|
6700
6975
|
actor,
|
|
6701
|
-
`import config snapshot from ${
|
|
6976
|
+
`import config snapshot from ${import_node_path13.default.basename(sourcePath)}`,
|
|
6702
6977
|
JSON.stringify({
|
|
6703
6978
|
type: "config_snapshot_import",
|
|
6704
6979
|
sourcePath,
|
|
@@ -6712,7 +6987,7 @@ async function runConfigImportCommand(options) {
|
|
|
6712
6987
|
output.write(
|
|
6713
6988
|
[
|
|
6714
6989
|
`Imported config snapshot from ${sourcePath}`,
|
|
6715
|
-
`- updated .env in ${
|
|
6990
|
+
`- updated .env in ${import_node_path13.default.resolve(cwd, ".env")}`,
|
|
6716
6991
|
`- synchronized room settings: ${normalizedRooms.length}`,
|
|
6717
6992
|
"- restart required: yes (global env settings are restart-scoped)"
|
|
6718
6993
|
].join("\n") + "\n"
|
|
@@ -6763,6 +7038,10 @@ function buildSnapshotEnv(config) {
|
|
|
6763
7038
|
CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT: String(config.cliCompat.disableReplyChunkSplit),
|
|
6764
7039
|
CLI_COMPAT_PROGRESS_THROTTLE_MS: String(config.cliCompat.progressThrottleMs),
|
|
6765
7040
|
CLI_COMPAT_FETCH_MEDIA: String(config.cliCompat.fetchMedia),
|
|
7041
|
+
CLI_COMPAT_TRANSCRIBE_AUDIO: String(config.cliCompat.transcribeAudio),
|
|
7042
|
+
CLI_COMPAT_AUDIO_TRANSCRIBE_MODEL: config.cliCompat.audioTranscribeModel,
|
|
7043
|
+
CLI_COMPAT_AUDIO_TRANSCRIBE_TIMEOUT_MS: String(config.cliCompat.audioTranscribeTimeoutMs),
|
|
7044
|
+
CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS: String(config.cliCompat.audioTranscribeMaxChars),
|
|
6766
7045
|
CLI_COMPAT_RECORD_PATH: config.cliCompat.recordPath ?? "",
|
|
6767
7046
|
DOCTOR_HTTP_TIMEOUT_MS: String(config.doctorHttpTimeoutMs),
|
|
6768
7047
|
ADMIN_BIND_HOST: config.adminBindHost,
|
|
@@ -6788,10 +7067,10 @@ function parseJsonFile(filePath) {
|
|
|
6788
7067
|
function normalizeSnapshotEnv(env, cwd) {
|
|
6789
7068
|
return {
|
|
6790
7069
|
...env,
|
|
6791
|
-
CODEX_WORKDIR:
|
|
6792
|
-
STATE_DB_PATH:
|
|
6793
|
-
STATE_PATH: env.STATE_PATH.trim() ?
|
|
6794
|
-
CLI_COMPAT_RECORD_PATH: env.CLI_COMPAT_RECORD_PATH.trim() ?
|
|
7070
|
+
CODEX_WORKDIR: import_node_path13.default.resolve(cwd, env.CODEX_WORKDIR),
|
|
7071
|
+
STATE_DB_PATH: import_node_path13.default.resolve(cwd, env.STATE_DB_PATH),
|
|
7072
|
+
STATE_PATH: env.STATE_PATH.trim() ? import_node_path13.default.resolve(cwd, env.STATE_PATH) : "",
|
|
7073
|
+
CLI_COMPAT_RECORD_PATH: env.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path13.default.resolve(cwd, env.CLI_COMPAT_RECORD_PATH) : ""
|
|
6795
7074
|
};
|
|
6796
7075
|
}
|
|
6797
7076
|
function normalizeSnapshotRooms(rooms, cwd) {
|
|
@@ -6806,7 +7085,7 @@ function normalizeSnapshotRooms(rooms, cwd) {
|
|
|
6806
7085
|
throw new Error(`Duplicate roomId in snapshot: ${roomId}`);
|
|
6807
7086
|
}
|
|
6808
7087
|
seen.add(roomId);
|
|
6809
|
-
const workdir =
|
|
7088
|
+
const workdir = import_node_path13.default.resolve(cwd, room.workdir);
|
|
6810
7089
|
ensureDirectory2(workdir, `room workdir (${roomId})`);
|
|
6811
7090
|
normalized.push({
|
|
6812
7091
|
roomId,
|
|
@@ -6833,8 +7112,8 @@ function synchronizeRoomSettings(stateStore, rooms) {
|
|
|
6833
7112
|
}
|
|
6834
7113
|
}
|
|
6835
7114
|
function persistEnvSnapshot(cwd, env) {
|
|
6836
|
-
const envPath =
|
|
6837
|
-
const examplePath =
|
|
7115
|
+
const envPath = import_node_path13.default.resolve(cwd, ".env");
|
|
7116
|
+
const examplePath = import_node_path13.default.resolve(cwd, ".env.example");
|
|
6838
7117
|
const template = import_node_fs9.default.existsSync(envPath) ? import_node_fs9.default.readFileSync(envPath, "utf8") : import_node_fs9.default.existsSync(examplePath) ? import_node_fs9.default.readFileSync(examplePath, "utf8") : "";
|
|
6839
7118
|
const overrides = {};
|
|
6840
7119
|
for (const key of CONFIG_SNAPSHOT_ENV_KEYS) {
|
|
@@ -6919,7 +7198,7 @@ function jsonArrayStringSchema(key, allowEmpty) {
|
|
|
6919
7198
|
// src/preflight.ts
|
|
6920
7199
|
var import_node_child_process6 = require("child_process");
|
|
6921
7200
|
var import_node_fs10 = __toESM(require("fs"));
|
|
6922
|
-
var
|
|
7201
|
+
var import_node_path14 = __toESM(require("path"));
|
|
6923
7202
|
var import_node_util4 = require("util");
|
|
6924
7203
|
var execFileAsync4 = (0, import_node_util4.promisify)(import_node_child_process6.execFile);
|
|
6925
7204
|
var REQUIRED_ENV_KEYS = ["MATRIX_HOMESERVER", "MATRIX_USER_ID", "MATRIX_ACCESS_TOKEN"];
|
|
@@ -6930,7 +7209,7 @@ async function runStartupPreflight(options = {}) {
|
|
|
6930
7209
|
const fileExists = options.fileExists ?? import_node_fs10.default.existsSync;
|
|
6931
7210
|
const isDirectory = options.isDirectory ?? defaultIsDirectory;
|
|
6932
7211
|
const issues = [];
|
|
6933
|
-
const envPath =
|
|
7212
|
+
const envPath = import_node_path14.default.resolve(cwd, ".env");
|
|
6934
7213
|
let resolvedCodexBin = null;
|
|
6935
7214
|
let usedCodexFallback = false;
|
|
6936
7215
|
if (!fileExists(envPath)) {
|
|
@@ -7007,7 +7286,7 @@ async function runStartupPreflight(options = {}) {
|
|
|
7007
7286
|
}
|
|
7008
7287
|
}
|
|
7009
7288
|
const configuredWorkdir = readEnv(env, "CODEX_WORKDIR");
|
|
7010
|
-
const workdir =
|
|
7289
|
+
const workdir = import_node_path14.default.resolve(cwd, configuredWorkdir || cwd);
|
|
7011
7290
|
if (!fileExists(workdir) || !isDirectory(workdir)) {
|
|
7012
7291
|
issues.push({
|
|
7013
7292
|
level: "error",
|
|
@@ -7276,7 +7555,7 @@ function ensureRuntimeHomeOrExit() {
|
|
|
7276
7555
|
`);
|
|
7277
7556
|
process.exit(1);
|
|
7278
7557
|
}
|
|
7279
|
-
loadEnvFromFile(
|
|
7558
|
+
loadEnvFromFile(import_node_path15.default.resolve(home, ".env"));
|
|
7280
7559
|
runtimeHome = home;
|
|
7281
7560
|
return runtimeHome;
|
|
7282
7561
|
}
|
|
@@ -7291,7 +7570,7 @@ function parsePortOption(raw, fallback) {
|
|
|
7291
7570
|
}
|
|
7292
7571
|
function resolveCliVersion() {
|
|
7293
7572
|
try {
|
|
7294
|
-
const packagePath =
|
|
7573
|
+
const packagePath = import_node_path15.default.resolve(__dirname, "..", "package.json");
|
|
7295
7574
|
const content = import_node_fs11.default.readFileSync(packagePath, "utf8");
|
|
7296
7575
|
const parsed = JSON.parse(content);
|
|
7297
7576
|
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version : "0.0.0";
|
|
@@ -7302,9 +7581,9 @@ function resolveCliVersion() {
|
|
|
7302
7581
|
function resolveCliScriptPath() {
|
|
7303
7582
|
const argvPath = process.argv[1];
|
|
7304
7583
|
if (argvPath && argvPath.trim()) {
|
|
7305
|
-
return
|
|
7584
|
+
return import_node_path15.default.resolve(argvPath);
|
|
7306
7585
|
}
|
|
7307
|
-
return
|
|
7586
|
+
return import_node_path15.default.resolve(__dirname, "cli.js");
|
|
7308
7587
|
}
|
|
7309
7588
|
function maybeReexecServiceCommandWithSudo() {
|
|
7310
7589
|
if (typeof process.getuid !== "function" || process.getuid() === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeharbor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Instant-messaging bridge for Codex CLI sessions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
|
+
"scripts/postinstall-restart.cjs",
|
|
20
21
|
".env.example",
|
|
21
22
|
"README.md",
|
|
22
23
|
"LICENSE"
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
"e2e:install": "playwright install chromium",
|
|
56
57
|
"test:watch": "vitest test",
|
|
57
58
|
"test:legacy": "bash ./scripts/run-legacy-tests.sh",
|
|
59
|
+
"postinstall": "node ./scripts/postinstall-restart.cjs",
|
|
58
60
|
"prepare": "npm run build",
|
|
59
61
|
"prepublishOnly": "npm run changelog:check && npm run typecheck && npm run lint && npm run test:coverage && npm run build",
|
|
60
62
|
"changelog:check": "bash ./scripts/check-changelog.sh",
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { execFileSync } = require("node:child_process");
|
|
5
|
+
|
|
6
|
+
const LOG_PREFIX = "[codeharbor postinstall]";
|
|
7
|
+
const MAIN_SERVICE = "codeharbor.service";
|
|
8
|
+
const ADMIN_SERVICE = "codeharbor-admin.service";
|
|
9
|
+
|
|
10
|
+
function isTruthy(value) {
|
|
11
|
+
if (!value) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const normalized = String(value).trim().toLowerCase();
|
|
15
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isGlobalInstall() {
|
|
19
|
+
return isTruthy(process.env.npm_config_global);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hasRootPrivileges() {
|
|
23
|
+
if (typeof process.getuid !== "function") {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return process.getuid() === 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function commandExists(file) {
|
|
30
|
+
try {
|
|
31
|
+
execFileSync(file, ["--version"], { stdio: "ignore" });
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function runCommand(file, args, options) {
|
|
39
|
+
return execFileSync(file, args, {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
42
|
+
...options,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function listUnitFiles(unitName) {
|
|
47
|
+
try {
|
|
48
|
+
return runCommand("systemctl", ["list-unit-files", unitName, "--no-legend", "--no-pager"]);
|
|
49
|
+
} catch {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isUnitInstalled(unitName) {
|
|
55
|
+
const output = listUnitFiles(unitName).trim();
|
|
56
|
+
if (!output) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return output.split(/\r?\n/).some((line) => line.trim().startsWith(unitName));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isUnitActive(unitName) {
|
|
63
|
+
try {
|
|
64
|
+
const output = runCommand("systemctl", ["is-active", unitName], {}).trim();
|
|
65
|
+
return output === "active";
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function restartUnit(unitName) {
|
|
72
|
+
if (hasRootPrivileges()) {
|
|
73
|
+
runCommand("systemctl", ["restart", unitName], {});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
runCommand("sudo", ["-n", "systemctl", "restart", unitName], {});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function main() {
|
|
81
|
+
if (isTruthy(process.env.CODEHARBOR_SKIP_POSTINSTALL_RESTART)) {
|
|
82
|
+
console.log(`${LOG_PREFIX} skip restart: CODEHARBOR_SKIP_POSTINSTALL_RESTART is set.`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const forceRestart = isTruthy(process.env.CODEHARBOR_FORCE_POSTINSTALL_RESTART);
|
|
87
|
+
if (!forceRestart && !isGlobalInstall()) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (process.platform !== "linux") {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!commandExists("systemctl")) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const candidates = [MAIN_SERVICE, ADMIN_SERVICE].filter((unitName) => isUnitInstalled(unitName));
|
|
100
|
+
const activeUnits = candidates.filter((unitName) => isUnitActive(unitName));
|
|
101
|
+
if (activeUnits.length === 0) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const restarted = [];
|
|
106
|
+
const failed = [];
|
|
107
|
+
|
|
108
|
+
for (const unitName of activeUnits) {
|
|
109
|
+
try {
|
|
110
|
+
restartUnit(unitName);
|
|
111
|
+
restarted.push(unitName);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
failed.push({ unitName, error });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (restarted.length > 0) {
|
|
118
|
+
console.log(`${LOG_PREFIX} restarted: ${restarted.join(", ")}`);
|
|
119
|
+
}
|
|
120
|
+
if (failed.length > 0) {
|
|
121
|
+
for (const failure of failed) {
|
|
122
|
+
const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
|
|
123
|
+
console.warn(`${LOG_PREFIX} failed to restart ${failure.unitName}: ${message}`);
|
|
124
|
+
}
|
|
125
|
+
console.warn(`${LOG_PREFIX} run "codeharbor service restart --with-admin" manually if needed.`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
main();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
console.warn(`${LOG_PREFIX} unexpected error: ${message}`);
|
|
134
|
+
}
|