hanoman 0.1.13 → 0.1.15
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/dist/build-info.json +3 -3
- package/dist/cli.js +59 -21
- package/dist/server.js +667 -134
- package/docs/agent-integration.md +429 -0
- package/package.json +2 -1
- package/web/assets/{index-DRwKLKdp.js → index-DRv31V2x.js} +1540 -1536
- package/web/index.html +1 -1
package/dist/server.js
CHANGED
|
@@ -4164,16 +4164,34 @@ var init_enums = __esm({
|
|
|
4164
4164
|
}
|
|
4165
4165
|
});
|
|
4166
4166
|
|
|
4167
|
+
// ../shared/src/agent-engine.ts
|
|
4168
|
+
var zAgentEngine;
|
|
4169
|
+
var init_agent_engine = __esm({
|
|
4170
|
+
"../shared/src/agent-engine.ts"() {
|
|
4171
|
+
"use strict";
|
|
4172
|
+
init_zod();
|
|
4173
|
+
init_enums();
|
|
4174
|
+
zAgentEngine = external_exports.object({
|
|
4175
|
+
enabled: external_exports.boolean().default(false),
|
|
4176
|
+
agent: zAgent.default("claude"),
|
|
4177
|
+
model: external_exports.string().default("claude-opus-5"),
|
|
4178
|
+
effort: external_exports.string().default("xhigh")
|
|
4179
|
+
});
|
|
4180
|
+
}
|
|
4181
|
+
});
|
|
4182
|
+
|
|
4167
4183
|
// ../shared/src/telegram.ts
|
|
4168
4184
|
var zTelegramSettings, TELEGRAM_DEFAULTS, TELEGRAM_CONFIG_KEYS, zTelegramReadiness, zTelegramGatewayStatus, zTelegramMemoryRecord, zTelegramChatContext, TELEGRAM_REPLY_KINDS, zTelegramReplyKind, zTelegramConfirmationRequest, zTelegramReplyInput, zTelegramAuditRecord, zTerminalSteerInput;
|
|
4169
4185
|
var init_telegram = __esm({
|
|
4170
4186
|
"../shared/src/telegram.ts"() {
|
|
4171
4187
|
"use strict";
|
|
4172
4188
|
init_zod();
|
|
4189
|
+
init_agent_engine();
|
|
4173
4190
|
init_enums();
|
|
4174
4191
|
zTelegramSettings = external_exports.object({
|
|
4175
4192
|
enabled: external_exports.boolean().default(false),
|
|
4176
|
-
progress: external_exports.boolean().default(true)
|
|
4193
|
+
progress: external_exports.boolean().default(true),
|
|
4194
|
+
engine: zAgentEngine.default({})
|
|
4177
4195
|
});
|
|
4178
4196
|
TELEGRAM_DEFAULTS = zTelegramSettings.parse({});
|
|
4179
4197
|
TELEGRAM_CONFIG_KEYS = [
|
|
@@ -4306,6 +4324,9 @@ var init_auto_merge = __esm({
|
|
|
4306
4324
|
function codexModel(id) {
|
|
4307
4325
|
return CODEX_MODELS.find((m) => m.id === id);
|
|
4308
4326
|
}
|
|
4327
|
+
function codexEfforts(modelId) {
|
|
4328
|
+
return codexModel(modelId)?.efforts ?? E_BASE;
|
|
4329
|
+
}
|
|
4309
4330
|
function coerceCodexEffort(modelId, effort) {
|
|
4310
4331
|
const m = codexModel(modelId);
|
|
4311
4332
|
if (!m) return effort;
|
|
@@ -4320,13 +4341,14 @@ function cmpVersion(a, b) {
|
|
|
4320
4341
|
}
|
|
4321
4342
|
return 0;
|
|
4322
4343
|
}
|
|
4323
|
-
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSpec, NOTIFY_SOUNDS, MODELS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
4344
|
+
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSpec, NOTIFY_SOUNDS, MODELS, EFFORTS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
4324
4345
|
var init_entities = __esm({
|
|
4325
4346
|
"../shared/src/entities.ts"() {
|
|
4326
4347
|
"use strict";
|
|
4327
4348
|
init_zod();
|
|
4328
4349
|
init_enums();
|
|
4329
4350
|
init_telegram();
|
|
4351
|
+
init_agent_engine();
|
|
4330
4352
|
init_auto_merge();
|
|
4331
4353
|
zProject = external_exports.object({
|
|
4332
4354
|
id: external_exports.string(),
|
|
@@ -4420,6 +4442,7 @@ var init_entities = __esm({
|
|
|
4420
4442
|
{ id: "claude-haiku-4-5", label: "Haiku 4.5" },
|
|
4421
4443
|
{ id: "claude-fable-5", label: "Fable 5" }
|
|
4422
4444
|
];
|
|
4445
|
+
EFFORTS = ["xhigh", "high", "medium", "low", "max", "ultracode"];
|
|
4423
4446
|
E_5_6 = ["ultra", "max", "xhigh", "high", "medium", "low"];
|
|
4424
4447
|
E_LUNA = ["max", "xhigh", "high", "medium", "low"];
|
|
4425
4448
|
E_BASE = ["xhigh", "high", "medium", "low"];
|
|
@@ -4468,12 +4491,7 @@ var init_entities = __esm({
|
|
|
4468
4491
|
effort: external_exports.string().default("xhigh")
|
|
4469
4492
|
});
|
|
4470
4493
|
CONFLICT_DEFAULTS = zConflict.parse({});
|
|
4471
|
-
zLeadEngine =
|
|
4472
|
-
enabled: external_exports.boolean().default(false),
|
|
4473
|
-
agent: zAgent.default("claude"),
|
|
4474
|
-
model: external_exports.string().default("claude-opus-5"),
|
|
4475
|
-
effort: external_exports.string().default("xhigh")
|
|
4476
|
-
});
|
|
4494
|
+
zLeadEngine = zAgentEngine;
|
|
4477
4495
|
zLead = external_exports.object({
|
|
4478
4496
|
enabled: external_exports.boolean().default(false),
|
|
4479
4497
|
// master switch (AC-30)
|
|
@@ -5553,6 +5571,9 @@ var init_api = __esm({
|
|
|
5553
5571
|
limits: `${API}/limits`,
|
|
5554
5572
|
// SPEC-339 · versi codex CLI (peringatan lunak model GPT-5.6).
|
|
5555
5573
|
codexVersion: `${API}/codex/version`,
|
|
5574
|
+
// SPEC-489 · panduan AI agent — markdown MENTAH & PUBLIC (tanpa auth). Satu definisi URL untuk
|
|
5575
|
+
// klien web dan untuk tautan yang disalin operator ke agennya.
|
|
5576
|
+
agentDoc: `${API}/agent-integration.md`,
|
|
5556
5577
|
docs: (id) => `${API}/projects/${id}/docs`,
|
|
5557
5578
|
docFile: (id, path) => `${API}/projects/${id}/docs/${path}`,
|
|
5558
5579
|
// SPEC-210 · dokumen PRD project (freshest-wins: worktree sesi prd hidup > repoDir)
|
|
@@ -5879,6 +5900,7 @@ var init_config_registry = __esm({
|
|
|
5879
5900
|
kind: "url",
|
|
5880
5901
|
apply: "live",
|
|
5881
5902
|
category: "knob",
|
|
5903
|
+
example: "https://hanoman.nafanesia.id",
|
|
5882
5904
|
help: "Base URL hub tujuan sync (REST + WS). Kosong = instance ini murni HUB."
|
|
5883
5905
|
},
|
|
5884
5906
|
{
|
|
@@ -5888,6 +5910,7 @@ var init_config_registry = __esm({
|
|
|
5888
5910
|
kind: "secret",
|
|
5889
5911
|
apply: "live",
|
|
5890
5912
|
category: "credential",
|
|
5913
|
+
example: "43 karakter base64url dari tab Perangkat hub",
|
|
5891
5914
|
help: "Token yang diterbitkan hub (tab Perangkat di hub). Dikirim sebagai Bearer."
|
|
5892
5915
|
},
|
|
5893
5916
|
{
|
|
@@ -5897,6 +5920,7 @@ var init_config_registry = __esm({
|
|
|
5897
5920
|
kind: "int",
|
|
5898
5921
|
apply: "live",
|
|
5899
5922
|
category: "knob",
|
|
5923
|
+
example: "15000",
|
|
5900
5924
|
default: "15000",
|
|
5901
5925
|
min: 1e3
|
|
5902
5926
|
},
|
|
@@ -5908,13 +5932,14 @@ var init_config_registry = __esm({
|
|
|
5908
5932
|
kind: "secret",
|
|
5909
5933
|
apply: "new-session",
|
|
5910
5934
|
category: "credential",
|
|
5935
|
+
example: "sk-ant-oat01-\u2026",
|
|
5911
5936
|
inheritEnv: true,
|
|
5912
5937
|
help: "Token `claude setup-token`. Diwarisi proses claude yang di-spawn."
|
|
5913
5938
|
},
|
|
5914
|
-
{ key: "ANTHROPIC_API_KEY", group: "claude", label: "Anthropic API key", kind: "secret", apply: "new-session", category: "credential", inheritEnv: true },
|
|
5915
|
-
{ key: "HANOMAN_CLAUDE_BIN", group: "claude", label: "Biner claude", kind: "path", apply: "new-session", category: "knob", default: "claude" },
|
|
5939
|
+
{ key: "ANTHROPIC_API_KEY", group: "claude", label: "Anthropic API key", kind: "secret", apply: "new-session", category: "credential", example: "sk-ant-api03-\u2026", inheritEnv: true },
|
|
5940
|
+
{ key: "HANOMAN_CLAUDE_BIN", group: "claude", label: "Biner claude", kind: "path", apply: "new-session", category: "knob", example: "/opt/homebrew/bin/claude", default: "claude" },
|
|
5916
5941
|
// codex (SPEC-338 · ADR-0074)
|
|
5917
|
-
{ key: "HANOMAN_CODEX_BIN", group: "claude", label: "Biner codex", kind: "path", apply: "new-session", category: "knob", default: "codex" },
|
|
5942
|
+
{ key: "HANOMAN_CODEX_BIN", group: "claude", label: "Biner codex", kind: "path", apply: "new-session", category: "knob", example: "/opt/homebrew/bin/codex", default: "codex" },
|
|
5918
5943
|
{
|
|
5919
5944
|
key: "CLAUDE_CONFIG_DIR",
|
|
5920
5945
|
group: "claude",
|
|
@@ -5922,11 +5947,12 @@ var init_config_registry = __esm({
|
|
|
5922
5947
|
kind: "path",
|
|
5923
5948
|
apply: "new-session",
|
|
5924
5949
|
category: "knob",
|
|
5950
|
+
example: "~/.claude",
|
|
5925
5951
|
help: "Default ~/.claude. Sumber .credentials.json untuk panel usage/limit."
|
|
5926
5952
|
},
|
|
5927
5953
|
// vps
|
|
5928
|
-
{ key: "HANOMAN_SSH_KEY_DIR", group: "vps", label: "Dir key SSH", kind: "path", apply: "new-session", category: "knob", help: "Default ~/.hanoman." },
|
|
5929
|
-
{ key: "HANOMAN_SSH_BIN", group: "vps", label: "Biner ssh", kind: "path", apply: "new-session", category: "knob", default: "ssh" },
|
|
5954
|
+
{ key: "HANOMAN_SSH_KEY_DIR", group: "vps", label: "Dir key SSH", kind: "path", apply: "new-session", category: "knob", example: "~/.hanoman", help: "Default ~/.hanoman." },
|
|
5955
|
+
{ key: "HANOMAN_SSH_BIN", group: "vps", label: "Biner ssh", kind: "path", apply: "new-session", category: "knob", example: "/usr/bin/ssh", default: "ssh" },
|
|
5930
5956
|
// github (SPEC-471 · ADR-0095 · tarik issue → backlog). Dua mode auth, satu jalur kode:
|
|
5931
5957
|
// `gh` memakai keyring mesin; bila GITHUB_TOKEN diisi ia diteruskan sebagai GH_TOKEN ke
|
|
5932
5958
|
// proses gh DAN dipakai jalur REST (hub VPS yang tak punya keyring).
|
|
@@ -5937,6 +5963,7 @@ var init_config_registry = __esm({
|
|
|
5937
5963
|
kind: "secret",
|
|
5938
5964
|
apply: "live",
|
|
5939
5965
|
category: "credential",
|
|
5966
|
+
example: "ghp_\u2026",
|
|
5940
5967
|
help: "PAT scope `repo` (atau `public_repo`). Kosong = andalkan `gh auth login` di mesin ini."
|
|
5941
5968
|
},
|
|
5942
5969
|
{
|
|
@@ -5946,6 +5973,7 @@ var init_config_registry = __esm({
|
|
|
5946
5973
|
kind: "path",
|
|
5947
5974
|
apply: "live",
|
|
5948
5975
|
category: "knob",
|
|
5976
|
+
example: "/opt/homebrew/bin/gh",
|
|
5949
5977
|
default: "gh",
|
|
5950
5978
|
help: "Absen = tarik issue lewat HTTPS langsung ke api.github.com."
|
|
5951
5979
|
},
|
|
@@ -5958,6 +5986,7 @@ var init_config_registry = __esm({
|
|
|
5958
5986
|
kind: "secret",
|
|
5959
5987
|
apply: "live",
|
|
5960
5988
|
category: "credential",
|
|
5989
|
+
example: "123456789:AAE\u2026",
|
|
5961
5990
|
pattern: "^\\d{5,}:[A-Za-z0-9_-]{30,}$",
|
|
5962
5991
|
patternError: "format BotFather: <bot_id>:<secret>",
|
|
5963
5992
|
help: "Token dari BotFather. Disimpan terenkripsi; tak pernah dikembalikan utuh."
|
|
@@ -5969,6 +5998,7 @@ var init_config_registry = __esm({
|
|
|
5969
5998
|
kind: "secret",
|
|
5970
5999
|
apply: "live",
|
|
5971
6000
|
category: "credential",
|
|
6001
|
+
example: "hnm_agt_\u2026",
|
|
5972
6002
|
pattern: "^\\S{20,}$",
|
|
5973
6003
|
patternError: "plaintext AgentToken (hnm_agt_\u2026)",
|
|
5974
6004
|
help: "Plaintext AgentToken ber-capability Telegram. Dipakai session operator, bukan bot."
|
|
@@ -5980,6 +6010,7 @@ var init_config_registry = __esm({
|
|
|
5980
6010
|
kind: "string",
|
|
5981
6011
|
apply: "live",
|
|
5982
6012
|
category: "credential",
|
|
6013
|
+
example: "123456789, 987654321",
|
|
5983
6014
|
pattern: "^\\d+(?:[\\s,]+\\d+)*$",
|
|
5984
6015
|
patternError: "numeric user id, dipisah koma/spasi",
|
|
5985
6016
|
help: "Siapa yang boleh memerintah bot. Numeric Telegram user id, bukan username."
|
|
@@ -5991,12 +6022,13 @@ var init_config_registry = __esm({
|
|
|
5991
6022
|
kind: "string",
|
|
5992
6023
|
apply: "live",
|
|
5993
6024
|
category: "knob",
|
|
6025
|
+
example: "-1001234567890",
|
|
5994
6026
|
pattern: "^-?\\d+$",
|
|
5995
6027
|
patternError: "numeric chat id (channel/supergroup negatif)",
|
|
5996
6028
|
help: "Tujuan Test Connection. Kosong = pakai satu-satunya id di allowlist."
|
|
5997
6029
|
},
|
|
5998
6030
|
// runtime
|
|
5999
|
-
{ key: "HANOMAN_EVENTS_TICK_MS", group: "runtime", label: "Interval events (ms)", kind: "int", apply: "live", category: "knob", default: "1000", min: 100 },
|
|
6031
|
+
{ key: "HANOMAN_EVENTS_TICK_MS", group: "runtime", label: "Interval events (ms)", kind: "int", apply: "live", category: "knob", example: "1000", default: "1000", min: 100 },
|
|
6000
6032
|
{ key: "HANOMAN_UPDATE_FETCH", group: "runtime", label: "Deteksi update saat boot", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
6001
6033
|
// SPEC-398 · ADR-0087 · deteksi update kini membandingkan semver dengan registry npm, bukan
|
|
6002
6034
|
// menghitung commit di repo git (HANOMAN_REPO_ROOT dicabut bersama jalur git itu).
|
|
@@ -6007,6 +6039,7 @@ var init_config_registry = __esm({
|
|
|
6007
6039
|
kind: "string",
|
|
6008
6040
|
apply: "live",
|
|
6009
6041
|
category: "knob",
|
|
6042
|
+
example: "https://registry.npmjs.org",
|
|
6010
6043
|
default: "https://registry.npmjs.org",
|
|
6011
6044
|
help: "Sumber versi terbaru paket `hanoman`."
|
|
6012
6045
|
},
|
|
@@ -6017,15 +6050,16 @@ var init_config_registry = __esm({
|
|
|
6017
6050
|
kind: "string",
|
|
6018
6051
|
apply: "restart",
|
|
6019
6052
|
category: "knob",
|
|
6053
|
+
example: "hanoman",
|
|
6020
6054
|
default: "hanoman",
|
|
6021
6055
|
help: "Mengubah ini TIDAK memindahkan sesi tmux yang sudah hidup \u2014 berlaku setelah restart."
|
|
6022
6056
|
},
|
|
6023
6057
|
// gitGraph (SPEC-233 · preferensi tampilan git graph; semua live, dikonsumsi client)
|
|
6024
|
-
{ key: "gitGraph.style", group: "gitGraph", label: "Gaya graph", kind: "string", apply: "live", category: "knob", default: "rounded", help: "rounded | angular" },
|
|
6025
|
-
{ key: "gitGraph.colours", group: "gitGraph", label: "Warna lane (CSV)", kind: "string", apply: "live", category: "knob", help: "Daftar warna hex dipisah koma; kosong = palet bawaan." },
|
|
6026
|
-
{ key: "gitGraph.dateType", group: "gitGraph", label: "Jenis tanggal", kind: "string", apply: "live", category: "knob", default: "author", help: "author | commit" },
|
|
6027
|
-
{ key: "gitGraph.commitsInitialLoad", group: "gitGraph", label: "Muat awal", kind: "int", apply: "live", category: "knob", default: "200", min: 1 },
|
|
6028
|
-
{ key: "gitGraph.commitsLoadMore", group: "gitGraph", label: "Muat lagi", kind: "int", apply: "live", category: "knob", default: "100", min: 1 },
|
|
6058
|
+
{ key: "gitGraph.style", group: "gitGraph", label: "Gaya graph", kind: "string", apply: "live", category: "knob", example: "rounded", default: "rounded", help: "rounded | angular" },
|
|
6059
|
+
{ key: "gitGraph.colours", group: "gitGraph", label: "Warna lane (CSV)", kind: "string", apply: "live", category: "knob", example: "#c9a227, #6b8f71, #b4614e", help: "Daftar warna hex dipisah koma; kosong = palet bawaan." },
|
|
6060
|
+
{ key: "gitGraph.dateType", group: "gitGraph", label: "Jenis tanggal", kind: "string", apply: "live", category: "knob", example: "author", default: "author", help: "author | commit" },
|
|
6061
|
+
{ key: "gitGraph.commitsInitialLoad", group: "gitGraph", label: "Muat awal", kind: "int", apply: "live", category: "knob", example: "200", default: "200", min: 1 },
|
|
6062
|
+
{ key: "gitGraph.commitsLoadMore", group: "gitGraph", label: "Muat lagi", kind: "int", apply: "live", category: "knob", example: "100", default: "100", min: 1 },
|
|
6029
6063
|
{ key: "gitGraph.showRemoteBranches", group: "gitGraph", label: "Tampilkan remote", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
6030
6064
|
{ key: "gitGraph.showTags", group: "gitGraph", label: "Tampilkan tag", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
6031
6065
|
{ key: "gitGraph.showStashes", group: "gitGraph", label: "Tampilkan stash", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
@@ -6035,7 +6069,7 @@ var init_config_registry = __esm({
|
|
|
6035
6069
|
{ key: "gitGraph.combineLocalRemote", group: "gitGraph", label: "Gabung label local+remote", kind: "bool", apply: "live", category: "knob", default: "0" },
|
|
6036
6070
|
{ key: "gitGraph.markdown", group: "gitGraph", label: "Render markdown pesan", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
6037
6071
|
{ key: "gitGraph.emoji", group: "gitGraph", label: "Render emoji shortcode", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
6038
|
-
{ key: "gitGraph.issueLinkPattern", group: "gitGraph", label: "Pola link issue", kind: "string", apply: "live", category: "knob", help: "URL dengan $1 untuk nomor issue, mis. https://github.com/acme/app/issues/$1" },
|
|
6072
|
+
{ key: "gitGraph.issueLinkPattern", group: "gitGraph", label: "Pola link issue", kind: "string", apply: "live", category: "knob", example: "https://github.com/acme/app/issues/$1", help: "URL dengan $1 untuk nomor issue, mis. https://github.com/acme/app/issues/$1" },
|
|
6039
6073
|
// bootstrap (read-only)
|
|
6040
6074
|
{ key: "DATABASE_URL", group: "bootstrap", label: "DATABASE_URL", kind: "secret", apply: "restart", category: "bootstrap" },
|
|
6041
6075
|
{ key: "TEST_DATABASE_URL", group: "bootstrap", label: "TEST_DATABASE_URL", kind: "secret", apply: "restart", category: "bootstrap" },
|
|
@@ -7089,6 +7123,7 @@ var init_src = __esm({
|
|
|
7089
7123
|
"../shared/src/index.ts"() {
|
|
7090
7124
|
"use strict";
|
|
7091
7125
|
init_enums();
|
|
7126
|
+
init_agent_engine();
|
|
7092
7127
|
init_entities();
|
|
7093
7128
|
init_agent();
|
|
7094
7129
|
init_custom_agent();
|
|
@@ -7112,7 +7147,7 @@ var init_src = __esm({
|
|
|
7112
7147
|
|
|
7113
7148
|
// src/env.ts
|
|
7114
7149
|
import { readFileSync, existsSync } from "node:fs";
|
|
7115
|
-
import { dirname, resolve as
|
|
7150
|
+
import { dirname, resolve as resolve3 } from "node:path";
|
|
7116
7151
|
import { fileURLToPath } from "node:url";
|
|
7117
7152
|
var dir;
|
|
7118
7153
|
var init_env = __esm({
|
|
@@ -7120,7 +7155,7 @@ var init_env = __esm({
|
|
|
7120
7155
|
"use strict";
|
|
7121
7156
|
dir = dirname(fileURLToPath(import.meta.url));
|
|
7122
7157
|
for (let i = 0; i < 6; i++) {
|
|
7123
|
-
const file =
|
|
7158
|
+
const file = resolve3(dir, ".env");
|
|
7124
7159
|
if (existsSync(file)) {
|
|
7125
7160
|
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
7126
7161
|
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
@@ -7574,7 +7609,7 @@ ${lines2.join("\n")}` : "";
|
|
|
7574
7609
|
// ../runner/src/git.ts
|
|
7575
7610
|
import { spawnSync } from "node:child_process";
|
|
7576
7611
|
import { rmSync, mkdirSync, realpathSync } from "node:fs";
|
|
7577
|
-
import { isAbsolute, resolve as
|
|
7612
|
+
import { isAbsolute, resolve as resolve4 } from "node:path";
|
|
7578
7613
|
function git(cwd, args) {
|
|
7579
7614
|
const r = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
7580
7615
|
if (r.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${r.stderr || r.stdout || r.error?.message || "gagal spawn"}`);
|
|
@@ -7609,7 +7644,7 @@ var init_git = __esm({
|
|
|
7609
7644
|
addWorktree: (repo, path, branchFrom) => {
|
|
7610
7645
|
tryGit(repo, ["worktree", "remove", "--force", path]);
|
|
7611
7646
|
tryGit(repo, ["worktree", "prune"]);
|
|
7612
|
-
rmSync(isAbsolute(path) ? path :
|
|
7647
|
+
rmSync(isAbsolute(path) ? path : resolve4(repo, path), { recursive: true, force: true });
|
|
7613
7648
|
const base2 = resolveCommit(repo, branchFrom);
|
|
7614
7649
|
git(repo, ["worktree", "add", "--detach", path, base2]);
|
|
7615
7650
|
return base2;
|
|
@@ -7619,8 +7654,8 @@ var init_git = __esm({
|
|
|
7619
7654
|
// `fatal: not a working tree` → membuat DELETE /terminal/sessions balas 500. remove+prune+rm
|
|
7620
7655
|
// semuanya toleran, jadi penutupan sesi selalu 204 dan registrasi worktree tak stale (SPEC-197).
|
|
7621
7656
|
removeWorktree: (repo, path) => {
|
|
7622
|
-
const target2 = isAbsolute(path) ?
|
|
7623
|
-
if (target2 ===
|
|
7657
|
+
const target2 = isAbsolute(path) ? resolve4(path) : resolve4(repo, path);
|
|
7658
|
+
if (target2 === resolve4(repo)) {
|
|
7624
7659
|
throw new Error(`removeWorktree menolak menghapus repo itu sendiri: ${target2}`);
|
|
7625
7660
|
}
|
|
7626
7661
|
tryGit(repo, ["worktree", "remove", "--force", target2]);
|
|
@@ -7936,7 +7971,7 @@ var init_custom_agents = __esm({
|
|
|
7936
7971
|
|
|
7937
7972
|
// ../runner/src/paths.ts
|
|
7938
7973
|
import { homedir } from "node:os";
|
|
7939
|
-
import { dirname as dirname2, isAbsolute as isAbsolute2, join, resolve as
|
|
7974
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join, resolve as resolve5 } from "node:path";
|
|
7940
7975
|
function resolveHome(env = process.env, home2 = homedir()) {
|
|
7941
7976
|
const v = env.HANOMAN_HOME?.trim();
|
|
7942
7977
|
return v ? v : join(home2, ".hanoman");
|
|
@@ -7961,7 +7996,7 @@ function resolveDbUrl(env, schemaDir2) {
|
|
|
7961
7996
|
function absoluteFileUrl(raw, schemaDir2) {
|
|
7962
7997
|
const p = raw.slice("file:".length);
|
|
7963
7998
|
if (p.startsWith(":")) return raw;
|
|
7964
|
-
return `file:${isAbsolute2(p) ? p :
|
|
7999
|
+
return `file:${isAbsolute2(p) ? p : resolve5(schemaDir2, p)}`;
|
|
7965
8000
|
}
|
|
7966
8001
|
function dbUrlNotice(env) {
|
|
7967
8002
|
if (env.HANOMAN_DATABASE_URL?.trim()) return null;
|
|
@@ -8011,6 +8046,7 @@ function buildTelegramOperatorPrompt(input) {
|
|
|
8011
8046
|
"",
|
|
8012
8047
|
"- Bahasa natural adalah antarmuka utama; command hanya shortcut yang tetap kamu tangani di sesi ini.",
|
|
8013
8048
|
`- Command minimum: ${COMMANDS.join(", ")}.`,
|
|
8049
|
+
`- Command runtime sesi ini ditangani server, bukan kamu: ${SERVER_COMMANDS.join(", ")}. Sebutkan di /help apa adanya; jangan pernah mencoba menjawabnya sendiri.`,
|
|
8014
8050
|
`- Panggil API dengan base $HANOMAN_API_BASE, bearer $HANOMAN_TELEGRAM_AGENT_TOKEN, dan header x-hanoman-telegram-update: ${update2.updateId}.`,
|
|
8015
8051
|
"- Terbitkan jawaban user-facing secara eksplisit melalui POST $HANOMAN_API_BASE/api/telegram/replies.",
|
|
8016
8052
|
"- Kind reply hanya progress|final|decision|failure|confirmation. Jangan mengandalkan layar PTY sebagai balasan.",
|
|
@@ -8024,7 +8060,7 @@ function buildTelegramOperatorPrompt(input) {
|
|
|
8024
8060
|
update2.text
|
|
8025
8061
|
].join("\n");
|
|
8026
8062
|
}
|
|
8027
|
-
var COMMANDS;
|
|
8063
|
+
var COMMANDS, SERVER_COMMANDS;
|
|
8028
8064
|
var init_telegram_operator = __esm({
|
|
8029
8065
|
"../runner/src/telegram-operator.ts"() {
|
|
8030
8066
|
"use strict";
|
|
@@ -8042,6 +8078,12 @@ var init_telegram_operator = __esm({
|
|
|
8042
8078
|
"/personality [nama|reset]",
|
|
8043
8079
|
"/skills"
|
|
8044
8080
|
];
|
|
8081
|
+
SERVER_COMMANDS = [
|
|
8082
|
+
"/engine",
|
|
8083
|
+
"/runtime claude|codex",
|
|
8084
|
+
"/model <id>",
|
|
8085
|
+
"/effort <nilai>"
|
|
8086
|
+
];
|
|
8045
8087
|
}
|
|
8046
8088
|
});
|
|
8047
8089
|
|
|
@@ -8197,7 +8239,7 @@ var init_tap = __esm({
|
|
|
8197
8239
|
|
|
8198
8240
|
// src/db.ts
|
|
8199
8241
|
import { mkdirSync as mkdirSync2 } from "node:fs";
|
|
8200
|
-
import { dirname as dirname3, resolve as
|
|
8242
|
+
import { dirname as dirname3, resolve as resolve6 } from "node:path";
|
|
8201
8243
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8202
8244
|
import { PrismaClient } from "@prisma/client";
|
|
8203
8245
|
var schemaDir, notice, url, base, prisma;
|
|
@@ -8207,7 +8249,7 @@ var init_db = __esm({
|
|
|
8207
8249
|
init_env();
|
|
8208
8250
|
init_src2();
|
|
8209
8251
|
init_tap();
|
|
8210
|
-
schemaDir =
|
|
8252
|
+
schemaDir = resolve6(dirname3(fileURLToPath2(import.meta.url)), "../prisma");
|
|
8211
8253
|
notice = dbUrlNotice(process.env);
|
|
8212
8254
|
if (notice) console.warn(notice);
|
|
8213
8255
|
url = resolveDbUrl(process.env, schemaDir);
|
|
@@ -9781,10 +9823,11 @@ var init_client = __esm({
|
|
|
9781
9823
|
"src/services/telegram/client.ts"() {
|
|
9782
9824
|
"use strict";
|
|
9783
9825
|
TelegramApiError = class extends Error {
|
|
9784
|
-
constructor(method, code, message) {
|
|
9826
|
+
constructor(method, code, message, retryAfter) {
|
|
9785
9827
|
super(`Telegram ${method} gagal${code ? ` (${code})` : ""}: ${message}`);
|
|
9786
9828
|
this.method = method;
|
|
9787
9829
|
this.code = code;
|
|
9830
|
+
this.retryAfter = retryAfter;
|
|
9788
9831
|
this.name = "TelegramApiError";
|
|
9789
9832
|
}
|
|
9790
9833
|
};
|
|
@@ -9809,7 +9852,10 @@ var init_client = __esm({
|
|
|
9809
9852
|
} catch (error) {
|
|
9810
9853
|
throw new TelegramApiError(method, void 0, this.safe(error.message || "network error"));
|
|
9811
9854
|
}
|
|
9812
|
-
if (!response.ok)
|
|
9855
|
+
if (!response.ok) {
|
|
9856
|
+
const failed = await response.json().catch(() => null);
|
|
9857
|
+
throw new TelegramApiError(method, response.status, `HTTP ${response.status}`, failed?.parameters?.retry_after);
|
|
9858
|
+
}
|
|
9813
9859
|
let envelope;
|
|
9814
9860
|
try {
|
|
9815
9861
|
envelope = await response.json();
|
|
@@ -9817,7 +9863,12 @@ var init_client = __esm({
|
|
|
9817
9863
|
throw new TelegramApiError(method, response.status, "respons bukan JSON");
|
|
9818
9864
|
}
|
|
9819
9865
|
if (!envelope.ok || envelope.result === void 0) {
|
|
9820
|
-
throw new TelegramApiError(
|
|
9866
|
+
throw new TelegramApiError(
|
|
9867
|
+
method,
|
|
9868
|
+
envelope.error_code,
|
|
9869
|
+
this.safe(envelope.description ?? "respons tidak valid"),
|
|
9870
|
+
envelope.parameters?.retry_after
|
|
9871
|
+
);
|
|
9821
9872
|
}
|
|
9822
9873
|
return envelope.result;
|
|
9823
9874
|
}
|
|
@@ -9842,6 +9893,14 @@ var init_client = __esm({
|
|
|
9842
9893
|
...input.replyMarkup ? { reply_markup: input.replyMarkup } : {}
|
|
9843
9894
|
});
|
|
9844
9895
|
}
|
|
9896
|
+
/**
|
|
9897
|
+
* SPEC-493 · indikator "typing…". Argumennya posisional (beda dari tetangganya) sesuai kontrak
|
|
9898
|
+
* yang diminta. Telegram TAK punya API stop-typing: statusnya padam sendiri ~5 detik sesudah
|
|
9899
|
+
* panggilan terakhir, dan pesan masuk apa pun langsung menghapusnya.
|
|
9900
|
+
*/
|
|
9901
|
+
sendChatAction(chatId, action = "typing") {
|
|
9902
|
+
return this.call("sendChatAction", { chat_id: chatId, action });
|
|
9903
|
+
}
|
|
9845
9904
|
editMessageText(input) {
|
|
9846
9905
|
return this.call("editMessageText", {
|
|
9847
9906
|
chat_id: input.chatId,
|
|
@@ -9859,6 +9918,40 @@ var init_client = __esm({
|
|
|
9859
9918
|
}
|
|
9860
9919
|
});
|
|
9861
9920
|
|
|
9921
|
+
// src/services/telegram/config.ts
|
|
9922
|
+
async function setTelegramEngine(next) {
|
|
9923
|
+
const cur = await getSetting();
|
|
9924
|
+
const data = { ...cur, telegram: { ...cur.telegram, engine: next } };
|
|
9925
|
+
await prisma.setting.upsert({ where: { id: 1 }, update: { data }, create: { id: 1, data } });
|
|
9926
|
+
return next;
|
|
9927
|
+
}
|
|
9928
|
+
async function telegramAgentDefaults() {
|
|
9929
|
+
const e = (await getSetting()).telegram.engine;
|
|
9930
|
+
if (!e.enabled) return sessionAgentDefaults();
|
|
9931
|
+
return e.agent === "codex" ? { agent: "codex", model: e.model, effort: coerceCodexEffort(e.model, e.effort) } : { agent: "claude", model: e.model, effort: e.effort };
|
|
9932
|
+
}
|
|
9933
|
+
async function telegramEngineContext() {
|
|
9934
|
+
const s2 = await getSetting();
|
|
9935
|
+
return {
|
|
9936
|
+
enabled: s2.telegram.engine.enabled,
|
|
9937
|
+
effective: await telegramAgentDefaults(),
|
|
9938
|
+
claude: { model: s2.model, effort: s2.effort },
|
|
9939
|
+
codex: { model: s2.codex.model, effort: s2.codex.effort }
|
|
9940
|
+
};
|
|
9941
|
+
}
|
|
9942
|
+
function telegramReloadNeeded(before, after) {
|
|
9943
|
+
const strip = ({ engine: _engine, ...rest }) => rest;
|
|
9944
|
+
return JSON.stringify(strip(before)) !== JSON.stringify(strip(after));
|
|
9945
|
+
}
|
|
9946
|
+
var init_config3 = __esm({
|
|
9947
|
+
"src/services/telegram/config.ts"() {
|
|
9948
|
+
"use strict";
|
|
9949
|
+
init_src();
|
|
9950
|
+
init_db();
|
|
9951
|
+
init_settings2();
|
|
9952
|
+
}
|
|
9953
|
+
});
|
|
9954
|
+
|
|
9862
9955
|
// src/services/telegram/protocol.ts
|
|
9863
9956
|
import { createHash as createHash2 } from "node:crypto";
|
|
9864
9957
|
function normalizeTelegramCommand(text) {
|
|
@@ -9959,7 +10052,7 @@ function splitTelegramText(text, max = 4096, exactSecrets = []) {
|
|
|
9959
10052
|
if (rest) chunks.push(rest);
|
|
9960
10053
|
return chunks;
|
|
9961
10054
|
}
|
|
9962
|
-
var record, ANSI, CONTROLS, CREDENTIAL_PATTERNS;
|
|
10055
|
+
var record, ANSI, CONTROLS, CREDENTIAL_PATTERNS, TELEGRAM_GATEWAY_FAILURE_KIND, TELEGRAM_FINAL_REPLY_KINDS;
|
|
9963
10056
|
var init_protocol = __esm({
|
|
9964
10057
|
"src/services/telegram/protocol.ts"() {
|
|
9965
10058
|
"use strict";
|
|
@@ -9971,6 +10064,14 @@ var init_protocol = __esm({
|
|
|
9971
10064
|
/\bhnm_(?:agent_)?[A-Za-z0-9_-]{16,}\b/g,
|
|
9972
10065
|
/\bsk-ant-[A-Za-z0-9_-]{16,}\b/g
|
|
9973
10066
|
];
|
|
10067
|
+
TELEGRAM_GATEWAY_FAILURE_KIND = "gateway-failure";
|
|
10068
|
+
TELEGRAM_FINAL_REPLY_KINDS = /* @__PURE__ */ new Set([
|
|
10069
|
+
"final",
|
|
10070
|
+
"decision",
|
|
10071
|
+
"failure",
|
|
10072
|
+
"confirmation",
|
|
10073
|
+
TELEGRAM_GATEWAY_FAILURE_KIND
|
|
10074
|
+
]);
|
|
9974
10075
|
}
|
|
9975
10076
|
});
|
|
9976
10077
|
|
|
@@ -10022,6 +10123,69 @@ var init_runtime = __esm({
|
|
|
10022
10123
|
}
|
|
10023
10124
|
});
|
|
10024
10125
|
|
|
10126
|
+
// src/services/telegram/typing.ts
|
|
10127
|
+
var TYPING_ACTIVE_POLL_SEC, TYPING_MIN_INTERVAL_MS, TYPING_COOLDOWN_BASE_MS, TYPING_COOLDOWN_MIN_MS, TYPING_COOLDOWN_MAX_MS, TYPING_MAX_WAIT_MS, clampTypingCooldown, pollTimeoutFor, TelegramTypingIndicator;
|
|
10128
|
+
var init_typing = __esm({
|
|
10129
|
+
"src/services/telegram/typing.ts"() {
|
|
10130
|
+
"use strict";
|
|
10131
|
+
init_client();
|
|
10132
|
+
TYPING_ACTIVE_POLL_SEC = 4;
|
|
10133
|
+
TYPING_MIN_INTERVAL_MS = 3e3;
|
|
10134
|
+
TYPING_COOLDOWN_BASE_MS = 5e3;
|
|
10135
|
+
TYPING_COOLDOWN_MIN_MS = 1e3;
|
|
10136
|
+
TYPING_COOLDOWN_MAX_MS = 3e5;
|
|
10137
|
+
TYPING_MAX_WAIT_MS = 6e5;
|
|
10138
|
+
clampTypingCooldown = (ms) => Math.min(TYPING_COOLDOWN_MAX_MS, Math.max(TYPING_COOLDOWN_MIN_MS, Math.round(ms)));
|
|
10139
|
+
pollTimeoutFor = (waiting, idle) => waiting > 0 ? Math.min(TYPING_ACTIVE_POLL_SEC, idle) : idle;
|
|
10140
|
+
TelegramTypingIndicator = class {
|
|
10141
|
+
constructor(deps) {
|
|
10142
|
+
this.deps = deps;
|
|
10143
|
+
this.now = deps.now ?? (() => Date.now());
|
|
10144
|
+
}
|
|
10145
|
+
state = /* @__PURE__ */ new Map();
|
|
10146
|
+
now;
|
|
10147
|
+
/** Paksa — MENGABAIKAN throttle. Telegram menghapus status typing tiap ada pesan masuk. */
|
|
10148
|
+
async arm(chatId) {
|
|
10149
|
+
await this.send(chatId, true);
|
|
10150
|
+
}
|
|
10151
|
+
/** Denyut loop; ter-throttle supaya iterasi cepat beruntun tak jadi banjir panggilan API. */
|
|
10152
|
+
async refresh(chatIds) {
|
|
10153
|
+
for (const chatId of chatIds) await this.send(chatId, false);
|
|
10154
|
+
this.prune(new Set(chatIds));
|
|
10155
|
+
}
|
|
10156
|
+
async send(chatId, force) {
|
|
10157
|
+
if (!this.deps.enabled) return;
|
|
10158
|
+
const now = this.now();
|
|
10159
|
+
const current = this.state.get(chatId);
|
|
10160
|
+
if (current && now < current.cooldownUntil) return;
|
|
10161
|
+
if (!force && current && now - current.lastArmedAt < TYPING_MIN_INTERVAL_MS) return;
|
|
10162
|
+
try {
|
|
10163
|
+
await this.deps.client.sendChatAction(chatId, "typing");
|
|
10164
|
+
this.state.set(chatId, { lastArmedAt: now, cooldownUntil: 0, nextDelayMs: TYPING_COOLDOWN_BASE_MS });
|
|
10165
|
+
} catch (error) {
|
|
10166
|
+
const retryAfterMs = error instanceof TelegramApiError && typeof error.retryAfter === "number" && error.retryAfter > 0 ? error.retryAfter * 1e3 : null;
|
|
10167
|
+
const delay = clampTypingCooldown(retryAfterMs ?? current?.nextDelayMs ?? TYPING_COOLDOWN_BASE_MS);
|
|
10168
|
+
this.state.set(chatId, {
|
|
10169
|
+
lastArmedAt: current?.lastArmedAt ?? 0,
|
|
10170
|
+
cooldownUntil: now + delay,
|
|
10171
|
+
nextDelayMs: clampTypingCooldown(delay * 2)
|
|
10172
|
+
});
|
|
10173
|
+
}
|
|
10174
|
+
}
|
|
10175
|
+
/** Chat yang sudah lama tak aktif dan tak sedang di-cooldown dibuang agar peta tak tumbuh. */
|
|
10176
|
+
prune(active) {
|
|
10177
|
+
const now = this.now();
|
|
10178
|
+
for (const [chatId, entry] of this.state) {
|
|
10179
|
+
if (active.has(chatId)) continue;
|
|
10180
|
+
if (now < entry.cooldownUntil) continue;
|
|
10181
|
+
if (now - entry.lastArmedAt <= TYPING_MAX_WAIT_MS) continue;
|
|
10182
|
+
this.state.delete(chatId);
|
|
10183
|
+
}
|
|
10184
|
+
}
|
|
10185
|
+
};
|
|
10186
|
+
}
|
|
10187
|
+
});
|
|
10188
|
+
|
|
10025
10189
|
// src/services/telegram/gateway.ts
|
|
10026
10190
|
import { createHash as createHash3 } from "node:crypto";
|
|
10027
10191
|
var digestRaw, TelegramGateway;
|
|
@@ -10031,13 +10195,19 @@ var init_gateway = __esm({
|
|
|
10031
10195
|
init_client();
|
|
10032
10196
|
init_protocol();
|
|
10033
10197
|
init_runtime();
|
|
10198
|
+
init_typing();
|
|
10034
10199
|
digestRaw = (update2) => createHash3("sha256").update(JSON.stringify(update2)).digest("hex");
|
|
10035
10200
|
TelegramGateway = class {
|
|
10036
10201
|
constructor(deps) {
|
|
10037
10202
|
this.deps = deps;
|
|
10203
|
+
this.typing = deps.typing ?? new TelegramTypingIndicator({
|
|
10204
|
+
client: deps.client,
|
|
10205
|
+
enabled: deps.progress
|
|
10206
|
+
});
|
|
10038
10207
|
}
|
|
10039
10208
|
abort = null;
|
|
10040
10209
|
loopPromise = null;
|
|
10210
|
+
typing;
|
|
10041
10211
|
async processUpdates(updates) {
|
|
10042
10212
|
for (const raw of updates) await this.processUpdate(raw);
|
|
10043
10213
|
}
|
|
@@ -10113,11 +10283,15 @@ var init_gateway = __esm({
|
|
|
10113
10283
|
userId: input.userId,
|
|
10114
10284
|
updateId: input.updateId,
|
|
10115
10285
|
action: "dispatch",
|
|
10116
|
-
|
|
10286
|
+
// SPEC-492 · command runtime dijawab gateway sendiri; jejaknya harus bisa dibedakan dari
|
|
10287
|
+
// pesan yang benar-benar sampai ke sesi operator.
|
|
10288
|
+
outcome: target2.control ? "control" : target2.created ? "session-created" : "session-reused",
|
|
10117
10289
|
correlationId: `tg:${input.updateId}`
|
|
10118
10290
|
});
|
|
10291
|
+
await this.typing.arm(input.chatId);
|
|
10119
10292
|
updateTelegramRuntimeStatus({ lastUpdateAt: (/* @__PURE__ */ new Date()).toISOString(), lastError: null });
|
|
10120
10293
|
} catch (error) {
|
|
10294
|
+
const reason = sanitizeTelegramOutput(error.message, this.deps.exactSecrets).slice(0, 200);
|
|
10121
10295
|
await this.deps.store.markUpdateUncertain(input.updateId, "dispatch-failed");
|
|
10122
10296
|
await this.deps.store.audit({
|
|
10123
10297
|
chatId: input.chatId,
|
|
@@ -10127,7 +10301,14 @@ var init_gateway = __esm({
|
|
|
10127
10301
|
outcome: "uncertain",
|
|
10128
10302
|
correlationId: `tg:${input.updateId}`
|
|
10129
10303
|
});
|
|
10130
|
-
|
|
10304
|
+
await this.deps.store.enqueueReply({
|
|
10305
|
+
chatId: input.chatId,
|
|
10306
|
+
updateId: input.updateId,
|
|
10307
|
+
kind: TELEGRAM_GATEWAY_FAILURE_KIND,
|
|
10308
|
+
text: `Pesan Anda tertangkap tapi gagal diteruskan ke sesi operator: ${reason || "sebab tak diketahui"}. Kirim ulang setelah masalahnya beres \u2014 hanoman tidak pernah mengulanginya sendiri (at-most-once).`
|
|
10309
|
+
}).catch(() => {
|
|
10310
|
+
});
|
|
10311
|
+
updateTelegramRuntimeStatus({ lastError: reason });
|
|
10131
10312
|
}
|
|
10132
10313
|
}
|
|
10133
10314
|
async flushOutbox() {
|
|
@@ -10153,6 +10334,7 @@ var init_gateway = __esm({
|
|
|
10153
10334
|
} : void 0;
|
|
10154
10335
|
const message = await this.deps.client.sendMessage({ chatId: row.chatId, text: chunks[index], replyMarkup });
|
|
10155
10336
|
messageId = message.message_id;
|
|
10337
|
+
if (!(isLast && TELEGRAM_FINAL_REPLY_KINDS.has(row.kind))) await this.typing.arm(row.chatId);
|
|
10156
10338
|
}
|
|
10157
10339
|
await this.deps.store.markOutboxSent(row.id, messageId);
|
|
10158
10340
|
} catch (error) {
|
|
@@ -10178,22 +10360,31 @@ var init_gateway = __esm({
|
|
|
10178
10360
|
});
|
|
10179
10361
|
}
|
|
10180
10362
|
async loop(signal) {
|
|
10363
|
+
const idlePollTimeout = this.deps.pollTimeout ?? 25;
|
|
10181
10364
|
while (!signal.aborted) {
|
|
10182
10365
|
try {
|
|
10366
|
+
const waiting = await this.deps.store.chatsAwaitingReply(new Date(Date.now() - TYPING_MAX_WAIT_MS));
|
|
10367
|
+
await this.typing.refresh(waiting);
|
|
10183
10368
|
const updates = await this.deps.client.getUpdates({
|
|
10184
10369
|
offset: await this.deps.store.offset(),
|
|
10185
10370
|
limit: 100,
|
|
10186
|
-
timeout:
|
|
10371
|
+
timeout: pollTimeoutFor(waiting.length, idlePollTimeout),
|
|
10187
10372
|
signal
|
|
10188
10373
|
});
|
|
10189
|
-
|
|
10190
|
-
|
|
10374
|
+
if (telegramRuntimeStatus().readiness === "error") {
|
|
10375
|
+
updateTelegramRuntimeStatus({ running: true, readiness: "running", lastError: null });
|
|
10376
|
+
}
|
|
10377
|
+
try {
|
|
10378
|
+
await this.processUpdates(updates);
|
|
10379
|
+
} finally {
|
|
10380
|
+
await this.flushOutbox();
|
|
10381
|
+
}
|
|
10191
10382
|
} catch (error) {
|
|
10192
10383
|
if (signal.aborted) return;
|
|
10193
10384
|
const message = sanitizeTelegramOutput(error.message, this.deps.exactSecrets).slice(0, 500);
|
|
10194
10385
|
updateTelegramRuntimeStatus({ running: false, readiness: "error", lastError: message });
|
|
10195
10386
|
if (error instanceof TelegramApiError && error.code === 409) return;
|
|
10196
|
-
await new Promise((
|
|
10387
|
+
await new Promise((resolve15) => setTimeout(resolve15, 1e3));
|
|
10197
10388
|
}
|
|
10198
10389
|
}
|
|
10199
10390
|
}
|
|
@@ -10205,6 +10396,112 @@ var init_gateway = __esm({
|
|
|
10205
10396
|
}
|
|
10206
10397
|
});
|
|
10207
10398
|
|
|
10399
|
+
// src/services/telegram/engine-command.ts
|
|
10400
|
+
function parseEngineCommand(text, ctx) {
|
|
10401
|
+
const match = text.trim().match(/^\/(engine|runtime|model|effort)(?:\s+([\s\S]+))?$/i);
|
|
10402
|
+
if (!match) return null;
|
|
10403
|
+
const verb = match[1].toLowerCase();
|
|
10404
|
+
const arg = (match[2] ?? "").trim();
|
|
10405
|
+
if (verb === "engine") {
|
|
10406
|
+
if (!arg) return { kind: "show" };
|
|
10407
|
+
const word = arg.toLowerCase();
|
|
10408
|
+
if (word === "off" || word === "mati") {
|
|
10409
|
+
return {
|
|
10410
|
+
kind: "set",
|
|
10411
|
+
engine: { enabled: false, ...ctx.effective },
|
|
10412
|
+
label: "Setelan operator Telegram \u2192 ikut default global sesi kerja"
|
|
10413
|
+
};
|
|
10414
|
+
}
|
|
10415
|
+
if (word === "restart" || word === "ulang") return { kind: "restart" };
|
|
10416
|
+
return { kind: "invalid", message: usage };
|
|
10417
|
+
}
|
|
10418
|
+
if (verb === "runtime") {
|
|
10419
|
+
const word = arg.toLowerCase();
|
|
10420
|
+
if (word !== "claude" && word !== "codex") {
|
|
10421
|
+
return { kind: "invalid", message: "Runtime hanya `claude` atau `codex`. Contoh: `/runtime codex`" };
|
|
10422
|
+
}
|
|
10423
|
+
const agent = word;
|
|
10424
|
+
const base2 = agent === "codex" ? ctx.codex : ctx.claude;
|
|
10425
|
+
const effort = agent === "codex" ? coerceCodexEffort(base2.model, base2.effort) : base2.effort;
|
|
10426
|
+
return {
|
|
10427
|
+
kind: "set",
|
|
10428
|
+
engine: { enabled: true, agent, model: base2.model, effort },
|
|
10429
|
+
label: `Runtime \u2192 ${AGENT_LABEL[agent]}`
|
|
10430
|
+
};
|
|
10431
|
+
}
|
|
10432
|
+
if (verb === "model") {
|
|
10433
|
+
const agent = ctx.effective.agent;
|
|
10434
|
+
const own = agent === "codex" ? codexModels() : claudeModels();
|
|
10435
|
+
const other = agent === "codex" ? claudeModels() : codexModels();
|
|
10436
|
+
if (!own.includes(arg)) {
|
|
10437
|
+
if (other.includes(arg)) {
|
|
10438
|
+
const swap = agent === "codex" ? "claude" : "codex";
|
|
10439
|
+
return {
|
|
10440
|
+
kind: "invalid",
|
|
10441
|
+
message: `\`${arg}\` adalah model ${AGENT_LABEL[swap]}, sedangkan runtime aktif ${AGENT_LABEL[agent]}. Jalankan \`/runtime ${swap}\` dulu.`
|
|
10442
|
+
};
|
|
10443
|
+
}
|
|
10444
|
+
return { kind: "invalid", message: `Model ${AGENT_LABEL[agent]} yang sah: ${own.join(", ")}.` };
|
|
10445
|
+
}
|
|
10446
|
+
const effort = agent === "codex" ? coerceCodexEffort(arg, ctx.effective.effort) : ctx.effective.effort;
|
|
10447
|
+
return {
|
|
10448
|
+
kind: "set",
|
|
10449
|
+
engine: { enabled: true, agent, model: arg, effort },
|
|
10450
|
+
label: `Model \u2192 ${arg}`
|
|
10451
|
+
};
|
|
10452
|
+
}
|
|
10453
|
+
const allowed = effortsFor(ctx.effective);
|
|
10454
|
+
if (!arg || !allowed.includes(arg)) {
|
|
10455
|
+
return {
|
|
10456
|
+
kind: "invalid",
|
|
10457
|
+
message: `Effort yang sah untuk \`${ctx.effective.model}\`: ${allowed.join(", ")}.`
|
|
10458
|
+
};
|
|
10459
|
+
}
|
|
10460
|
+
return {
|
|
10461
|
+
kind: "set",
|
|
10462
|
+
engine: { enabled: true, ...ctx.effective, effort: arg },
|
|
10463
|
+
label: `Effort \u2192 ${arg}`
|
|
10464
|
+
};
|
|
10465
|
+
}
|
|
10466
|
+
function formatEngineStatus(ctx, sessionAlive) {
|
|
10467
|
+
return [
|
|
10468
|
+
`Sesi operator berikutnya: ${triple(ctx.effective)}`,
|
|
10469
|
+
ctx.enabled ? "Sumber: setelan sendiri untuk kanal Telegram." : "Sumber: default global sesi kerja (belum pakai setelan sendiri).",
|
|
10470
|
+
sessionAlive ? "Sesi operator sekarang masih hidup dan tetap memakai setelan lamanya \u2014 `/engine restart` untuk menutupnya." : "Belum ada sesi operator yang hidup; pesan berikutnya lahir dengan setelan di atas.",
|
|
10471
|
+
"",
|
|
10472
|
+
usage
|
|
10473
|
+
].join("\n");
|
|
10474
|
+
}
|
|
10475
|
+
function formatEngineApplied(next, label, sessionAlive) {
|
|
10476
|
+
const head = next.enabled ? `${label}. Sesi operator berikutnya: ${triple(next)}` : `${label}. Sesi operator berikutnya mengikuti default global sesi kerja.`;
|
|
10477
|
+
return [
|
|
10478
|
+
head,
|
|
10479
|
+
sessionAlive ? "Sesi yang sedang jalan tidak diubah \u2014 ia satu proses, satu model seumur hidup. `/engine restart` menutupnya; ringkasan & memory tetap tersimpan." : "Berlaku untuk sesi operator berikutnya yang dibuat."
|
|
10480
|
+
].join("\n");
|
|
10481
|
+
}
|
|
10482
|
+
var AGENT_LABEL, TELEGRAM_CONTROL_KIND, claudeModels, codexModels, effortsFor, usage, triple;
|
|
10483
|
+
var init_engine_command = __esm({
|
|
10484
|
+
"src/services/telegram/engine-command.ts"() {
|
|
10485
|
+
"use strict";
|
|
10486
|
+
init_src();
|
|
10487
|
+
AGENT_LABEL = { claude: "Claude Code", codex: "Codex CLI" };
|
|
10488
|
+
TELEGRAM_CONTROL_KIND = "gateway-control";
|
|
10489
|
+
claudeModels = () => MODELS.map((m) => m.id);
|
|
10490
|
+
codexModels = () => CODEX_MODELS.map((m) => m.id);
|
|
10491
|
+
effortsFor = (t) => t.agent === "codex" ? codexEfforts(t.model) : EFFORTS;
|
|
10492
|
+
usage = [
|
|
10493
|
+
"Setelan runtime sesi operator Telegram:",
|
|
10494
|
+
"`/engine` \u2014 lihat setelan sekarang",
|
|
10495
|
+
"`/runtime claude|codex` \u2014 tukar runtime",
|
|
10496
|
+
"`/model <id>` \u2014 tukar model",
|
|
10497
|
+
"`/effort <nilai>` \u2014 tukar effort",
|
|
10498
|
+
"`/engine off` \u2014 kembali ikut default global sesi kerja",
|
|
10499
|
+
"`/engine restart` \u2014 tutup sesi operator supaya setelan berlaku sekarang"
|
|
10500
|
+
].join("\n");
|
|
10501
|
+
triple = (t) => `${AGENT_LABEL[t.agent]} \xB7 \`${t.model}\` \xB7 \`${t.effort}\``;
|
|
10502
|
+
}
|
|
10503
|
+
});
|
|
10504
|
+
|
|
10208
10505
|
// src/services/telegram/session.ts
|
|
10209
10506
|
import { createHash as createHash4 } from "node:crypto";
|
|
10210
10507
|
var chatHash, telegramOperatorSessionId, formatTelegramTurn, TelegramSessionCoordinator;
|
|
@@ -10212,6 +10509,7 @@ var init_session = __esm({
|
|
|
10212
10509
|
"src/services/telegram/session.ts"() {
|
|
10213
10510
|
"use strict";
|
|
10214
10511
|
init_src2();
|
|
10512
|
+
init_engine_command();
|
|
10215
10513
|
chatHash = (chatId) => createHash4("sha256").update(chatId).digest("hex").slice(0, 16);
|
|
10216
10514
|
telegramOperatorSessionId = (chatId) => `telegram-${chatHash(chatId)}`;
|
|
10217
10515
|
formatTelegramTurn = (input) => `[Telegram update ${input.updateId} \xB7 chat ${input.chatId} \xB7 kind ${input.kind}]
|
|
@@ -10221,15 +10519,19 @@ ${input.text}`;
|
|
|
10221
10519
|
this.deps = deps;
|
|
10222
10520
|
}
|
|
10223
10521
|
async dispatch(input) {
|
|
10522
|
+
if (input.kind === "command") {
|
|
10523
|
+
const handled = await this.control(input);
|
|
10524
|
+
if (handled) return handled;
|
|
10525
|
+
}
|
|
10224
10526
|
let context = await this.deps.store.chatContext(input.chatId);
|
|
10225
10527
|
if (!context) {
|
|
10226
|
-
const
|
|
10528
|
+
const seed = await this.deps.defaults();
|
|
10227
10529
|
await this.deps.store.ensureChat({
|
|
10228
10530
|
chatId: input.chatId,
|
|
10229
10531
|
userId: input.userId,
|
|
10230
|
-
agent:
|
|
10231
|
-
model:
|
|
10232
|
-
effort:
|
|
10532
|
+
agent: seed.agent,
|
|
10533
|
+
model: seed.model,
|
|
10534
|
+
effort: seed.effort
|
|
10233
10535
|
});
|
|
10234
10536
|
context = await this.deps.store.chatContext(input.chatId);
|
|
10235
10537
|
}
|
|
@@ -10243,11 +10545,13 @@ ${input.text}`;
|
|
|
10243
10545
|
if (context.sessionId !== sessionId2) await this.deps.store.bindSession(input.chatId, sessionId2);
|
|
10244
10546
|
return { sessionId: sessionId2, created: false };
|
|
10245
10547
|
}
|
|
10548
|
+
const engine = await this.deps.defaults();
|
|
10549
|
+
await this.deps.store.setChatEngine(input.chatId, engine);
|
|
10246
10550
|
const hash2 = chatHash(input.chatId);
|
|
10247
10551
|
const projectId = `telegram:${hash2}`;
|
|
10248
10552
|
const cwd = `${this.deps.home.replace(/\/$/, "")}/telegram/${hash2}`;
|
|
10249
10553
|
this.deps.ensureDir(cwd);
|
|
10250
|
-
if (
|
|
10554
|
+
if (engine.agent === "codex") this.deps.ensureCodexTrust(cwd);
|
|
10251
10555
|
const personality = await this.deps.personality(context.personalityAgentId, context.activeProjectId);
|
|
10252
10556
|
const prompt = buildTelegramOperatorPrompt({
|
|
10253
10557
|
update: input,
|
|
@@ -10258,9 +10562,9 @@ ${input.text}`;
|
|
|
10258
10562
|
const born = this.deps.port.createSession(projectId, cwd, {
|
|
10259
10563
|
id: sessionId2,
|
|
10260
10564
|
prompt,
|
|
10261
|
-
agent:
|
|
10262
|
-
model:
|
|
10263
|
-
effort:
|
|
10565
|
+
agent: engine.agent,
|
|
10566
|
+
model: engine.model,
|
|
10567
|
+
effort: engine.effort,
|
|
10264
10568
|
env: {
|
|
10265
10569
|
HANOMAN_API_BASE: this.deps.apiBase,
|
|
10266
10570
|
HANOMAN_TELEGRAM_AGENT_TOKEN: this.deps.agentToken,
|
|
@@ -10271,6 +10575,48 @@ ${input.text}`;
|
|
|
10271
10575
|
await this.deps.store.bindSession(input.chatId, sessionId2);
|
|
10272
10576
|
return { sessionId: sessionId2, created: true };
|
|
10273
10577
|
}
|
|
10578
|
+
/**
|
|
10579
|
+
* SPEC-492 · empat command runtime tak pernah menyentuh pane: ia soal transport, bukan isi
|
|
10580
|
+
* hanoman, dan harus tetap bekerja saat agennya justru macet. `null` = bukan command runtime →
|
|
10581
|
+
* pemanggil melanjutkan jalur lama persis seperti sebelumnya.
|
|
10582
|
+
*
|
|
10583
|
+
* Sengaja TIDAK mengetik `/model`/`/effort` ke pane hidup: ADR-0061 mencabut matrix per-fase
|
|
10584
|
+
* karena mekanisme itu tak andal, dan SPEC-487 mengukur kelasnya (ketikan ke pane yang sedang
|
|
10585
|
+
* menjalankan giliran mendarat sebagai pesan liar). Jalur yang dijanjikan ke operator adalah
|
|
10586
|
+
* `/engine restart` — deterministik, dan konteksnya selamat lewat ringkasan + curated memory.
|
|
10587
|
+
*/
|
|
10588
|
+
async control(input) {
|
|
10589
|
+
const ctx = await this.deps.engine.read();
|
|
10590
|
+
const cmd = parseEngineCommand(input.text, ctx);
|
|
10591
|
+
if (!cmd) return null;
|
|
10592
|
+
const sessionId2 = telegramOperatorSessionId(input.chatId);
|
|
10593
|
+
const live = this.deps.port.getSession(sessionId2);
|
|
10594
|
+
const alive = Boolean(live && !live.exited);
|
|
10595
|
+
let text;
|
|
10596
|
+
if (cmd.kind === "show") {
|
|
10597
|
+
text = formatEngineStatus(ctx, alive);
|
|
10598
|
+
} else if (cmd.kind === "invalid") {
|
|
10599
|
+
text = cmd.message;
|
|
10600
|
+
} else if (cmd.kind === "restart") {
|
|
10601
|
+
if (alive) {
|
|
10602
|
+
this.deps.port.killSession(sessionId2);
|
|
10603
|
+
await this.deps.store.patchChat(input.chatId, { sessionId: null });
|
|
10604
|
+
text = "Sesi operator ditutup. Pesan berikutnya lahir dengan setelan sekarang \u2014 ringkasan & curated memory tetap tersimpan.";
|
|
10605
|
+
} else {
|
|
10606
|
+
text = "Tak ada sesi operator yang sedang hidup. Pesan berikutnya lahir dengan setelan sekarang.";
|
|
10607
|
+
}
|
|
10608
|
+
} else {
|
|
10609
|
+
await this.deps.engine.write(cmd.engine);
|
|
10610
|
+
text = formatEngineApplied(cmd.engine, cmd.label, alive);
|
|
10611
|
+
}
|
|
10612
|
+
await this.deps.store.enqueueReply({
|
|
10613
|
+
chatId: input.chatId,
|
|
10614
|
+
updateId: input.updateId,
|
|
10615
|
+
kind: TELEGRAM_CONTROL_KIND,
|
|
10616
|
+
text
|
|
10617
|
+
});
|
|
10618
|
+
return { sessionId: sessionId2, created: false, control: true };
|
|
10619
|
+
}
|
|
10274
10620
|
};
|
|
10275
10621
|
}
|
|
10276
10622
|
});
|
|
@@ -10281,6 +10627,7 @@ var isUnique, TelegramStore;
|
|
|
10281
10627
|
var init_store = __esm({
|
|
10282
10628
|
"src/services/telegram/store.ts"() {
|
|
10283
10629
|
"use strict";
|
|
10630
|
+
init_protocol();
|
|
10284
10631
|
isUnique = (error) => error instanceof Prisma3.PrismaClientKnownRequestError && error.code === "P2002";
|
|
10285
10632
|
TelegramStore = class {
|
|
10286
10633
|
// SPEC-481 · klien yang diekspor kini ber-extension (tap webhook) sehingga tak lagi
|
|
@@ -10346,6 +10693,15 @@ var init_store = __esm({
|
|
|
10346
10693
|
update: { userId: input.userId }
|
|
10347
10694
|
});
|
|
10348
10695
|
}
|
|
10696
|
+
/**
|
|
10697
|
+
* SPEC-492 · cermin baris chat terhadap runtime yang BENAR-BENAR dipakai sesi operator terakhir.
|
|
10698
|
+
* `ensureChat` sengaja tak menyentuh ketiganya (ia hanya menyegarkan `userId`), jadi tanpa ini
|
|
10699
|
+
* `GET /telegram/chats/:chatId/context` — yang dibaca agen operator sendiri — melaporkan
|
|
10700
|
+
* snapshot dari chat pertama, selamanya.
|
|
10701
|
+
*/
|
|
10702
|
+
async setChatEngine(chatId, engine) {
|
|
10703
|
+
await this.db.telegramChat.update({ where: { chatId }, data: engine });
|
|
10704
|
+
}
|
|
10349
10705
|
async chatContext(chatId) {
|
|
10350
10706
|
const chat = await this.db.telegramChat.findUnique({ where: { chatId } });
|
|
10351
10707
|
if (!chat) return null;
|
|
@@ -10379,6 +10735,38 @@ var init_store = __esm({
|
|
|
10379
10735
|
data: { state: "uncertain", rejectReason: reason }
|
|
10380
10736
|
});
|
|
10381
10737
|
}
|
|
10738
|
+
/**
|
|
10739
|
+
* SPEC-493 · chat yang masih menunggu jawaban: ada `TelegramUpdate` `dispatched` sesudah `since`
|
|
10740
|
+
* yang belum punya baris outbox ber-kind final. Dihitung pada saat **enqueue**, bukan `sent` —
|
|
10741
|
+
* baris outbox lahir begitu session operator memanggil `POST /telegram/replies`, dan jarak
|
|
10742
|
+
* enqueue→kirim paling banyak satu iterasi loop. Menunggu `sent` akan menahan typing melewati
|
|
10743
|
+
* pesan finalnya sendiri.
|
|
10744
|
+
*
|
|
10745
|
+
* `since` adalah pagar keras: update yang session operatornya mati mengendap `dispatched`
|
|
10746
|
+
* SELAMANYA, dan tanpa pagar ini gateway akan mengetik selamanya sekaligus mengunci long-poll
|
|
10747
|
+
* di 4 detik selamanya.
|
|
10748
|
+
*/
|
|
10749
|
+
async chatsAwaitingReply(since) {
|
|
10750
|
+
const pending = await this.db.telegramUpdate.findMany({
|
|
10751
|
+
where: { state: "dispatched", chatId: { not: null }, dispatchedAt: { gte: since } },
|
|
10752
|
+
select: { updateId: true, chatId: true },
|
|
10753
|
+
orderBy: { updateId: "asc" }
|
|
10754
|
+
});
|
|
10755
|
+
if (!pending.length) return [];
|
|
10756
|
+
const answered = new Set((await this.db.telegramOutbox.findMany({
|
|
10757
|
+
where: {
|
|
10758
|
+
updateId: { in: pending.map((row) => row.updateId) },
|
|
10759
|
+
kind: { in: [...TELEGRAM_FINAL_REPLY_KINDS] }
|
|
10760
|
+
},
|
|
10761
|
+
select: { updateId: true }
|
|
10762
|
+
})).map((row) => row.updateId));
|
|
10763
|
+
const chats = [];
|
|
10764
|
+
for (const row of pending) {
|
|
10765
|
+
if (answered.has(row.updateId) || !row.chatId || chats.includes(row.chatId)) continue;
|
|
10766
|
+
chats.push(row.chatId);
|
|
10767
|
+
}
|
|
10768
|
+
return chats;
|
|
10769
|
+
}
|
|
10382
10770
|
async addMemory(chatId, content) {
|
|
10383
10771
|
return this.memoryView(await this.db.telegramMemory.create({ data: { chatId, content } }));
|
|
10384
10772
|
}
|
|
@@ -10513,7 +10901,8 @@ __export(bootstrap_exports, {
|
|
|
10513
10901
|
TELEGRAM_REQUIRED_CAPABILITIES: () => TELEGRAM_REQUIRED_CAPABILITIES,
|
|
10514
10902
|
installTelegramGateway: () => installTelegramGateway,
|
|
10515
10903
|
parseTelegramAllowedUserIds: () => parseTelegramAllowedUserIds,
|
|
10516
|
-
reloadTelegramGateway: () => reloadTelegramGateway
|
|
10904
|
+
reloadTelegramGateway: () => reloadTelegramGateway,
|
|
10905
|
+
telegramSessionDeps: () => telegramSessionDeps
|
|
10517
10906
|
});
|
|
10518
10907
|
import { mkdirSync as mkdirSync7 } from "node:fs";
|
|
10519
10908
|
function parseTelegramAllowedUserIds(raw) {
|
|
@@ -10522,14 +10911,14 @@ function parseTelegramAllowedUserIds(raw) {
|
|
|
10522
10911
|
if (ids.some((id) => !/^\d+$/.test(id))) throw new Error("Telegram allowlist hanya menerima numeric user id");
|
|
10523
10912
|
return new Set(ids);
|
|
10524
10913
|
}
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10914
|
+
function telegramSessionDeps(input) {
|
|
10915
|
+
return {
|
|
10916
|
+
store: input.store,
|
|
10917
|
+
port: { getSession, createSession, sendToPane, killSession },
|
|
10918
|
+
// SPEC-492 · BUKAN `sessionAgentDefaults`: sesi operator Telegram sebagian besar membaca API
|
|
10919
|
+
// lalu merangkum, bukan menulis kode, jadi ia boleh punya runtime/model/effort sendiri.
|
|
10920
|
+
defaults: telegramAgentDefaults,
|
|
10921
|
+
engine: { read: telegramEngineContext, write: setTelegramEngine },
|
|
10533
10922
|
personality: async (id, projectId) => {
|
|
10534
10923
|
if (!id) return null;
|
|
10535
10924
|
const row = await prisma.customAgent.findUnique({ where: { id } });
|
|
@@ -10541,7 +10930,17 @@ async function productionFactory(input) {
|
|
|
10541
10930
|
apiBase: input.apiBase,
|
|
10542
10931
|
agentToken: input.agentToken,
|
|
10543
10932
|
ensureDir: (path) => mkdirSync7(path, { recursive: true })
|
|
10544
|
-
}
|
|
10933
|
+
};
|
|
10934
|
+
}
|
|
10935
|
+
async function productionFactory(input) {
|
|
10936
|
+
const client = new TelegramApiClient(input.botToken);
|
|
10937
|
+
const me = await client.getMe();
|
|
10938
|
+
const store3 = new TelegramStore(prisma);
|
|
10939
|
+
const coordinator = new TelegramSessionCoordinator(telegramSessionDeps({
|
|
10940
|
+
apiBase: input.apiBase,
|
|
10941
|
+
agentToken: input.agentToken,
|
|
10942
|
+
store: store3
|
|
10943
|
+
}));
|
|
10545
10944
|
const gateway = new TelegramGateway({
|
|
10546
10945
|
client,
|
|
10547
10946
|
store: store3,
|
|
@@ -10653,6 +11052,7 @@ var init_bootstrap = __esm({
|
|
|
10653
11052
|
init_settings2();
|
|
10654
11053
|
init_pty();
|
|
10655
11054
|
init_client();
|
|
11055
|
+
init_config3();
|
|
10656
11056
|
init_gateway();
|
|
10657
11057
|
init_session();
|
|
10658
11058
|
init_store();
|
|
@@ -17222,17 +17622,17 @@ var require_stream_wormhole = __commonJS({
|
|
|
17222
17622
|
"../node_modules/.pnpm/stream-wormhole@1.1.0/node_modules/stream-wormhole/index.js"(exports, module) {
|
|
17223
17623
|
"use strict";
|
|
17224
17624
|
module.exports = (stream, throwError) => {
|
|
17225
|
-
return new Promise((
|
|
17625
|
+
return new Promise((resolve15, reject2) => {
|
|
17226
17626
|
if (typeof stream.resume !== "function") {
|
|
17227
|
-
return
|
|
17627
|
+
return resolve15();
|
|
17228
17628
|
}
|
|
17229
17629
|
stream.unpipe && stream.unpipe();
|
|
17230
17630
|
stream.resume();
|
|
17231
17631
|
if (stream._readableState && stream._readableState.ended) {
|
|
17232
|
-
return
|
|
17632
|
+
return resolve15();
|
|
17233
17633
|
}
|
|
17234
17634
|
if (!stream.readable || stream.destroyed) {
|
|
17235
|
-
return
|
|
17635
|
+
return resolve15();
|
|
17236
17636
|
}
|
|
17237
17637
|
function cleanup() {
|
|
17238
17638
|
stream.removeListener("end", onEnd);
|
|
@@ -17241,14 +17641,14 @@ var require_stream_wormhole = __commonJS({
|
|
|
17241
17641
|
}
|
|
17242
17642
|
function onEnd() {
|
|
17243
17643
|
cleanup();
|
|
17244
|
-
|
|
17644
|
+
resolve15();
|
|
17245
17645
|
}
|
|
17246
17646
|
function onError(err) {
|
|
17247
17647
|
cleanup();
|
|
17248
17648
|
if (throwError) {
|
|
17249
17649
|
reject2(err);
|
|
17250
17650
|
} else {
|
|
17251
|
-
|
|
17651
|
+
resolve15();
|
|
17252
17652
|
}
|
|
17253
17653
|
}
|
|
17254
17654
|
stream.on("end", onEnd);
|
|
@@ -17683,16 +18083,16 @@ var require_multipart2 = __commonJS({
|
|
|
17683
18083
|
}
|
|
17684
18084
|
};
|
|
17685
18085
|
const parts = () => {
|
|
17686
|
-
return new Promise((
|
|
18086
|
+
return new Promise((resolve15, reject2) => {
|
|
17687
18087
|
handle((val) => {
|
|
17688
18088
|
if (val instanceof Error) {
|
|
17689
18089
|
if (val.message === "Unexpected end of multipart data") {
|
|
17690
|
-
|
|
18090
|
+
resolve15(null);
|
|
17691
18091
|
} else {
|
|
17692
18092
|
reject2(val);
|
|
17693
18093
|
}
|
|
17694
18094
|
} else {
|
|
17695
|
-
|
|
18095
|
+
resolve15(val);
|
|
17696
18096
|
}
|
|
17697
18097
|
});
|
|
17698
18098
|
});
|
|
@@ -17975,11 +18375,39 @@ function pickWebDir(distDir, env, exists) {
|
|
|
17975
18375
|
return null;
|
|
17976
18376
|
}
|
|
17977
18377
|
|
|
18378
|
+
// src/guide-file.ts
|
|
18379
|
+
import { resolve as resolve2 } from "node:path";
|
|
18380
|
+
var AGENT_DOC_REL = "docs/agent-integration.md";
|
|
18381
|
+
function pickGuideFile(distDir, env, exists) {
|
|
18382
|
+
const forced = env.HANOMAN_AGENT_DOC?.trim();
|
|
18383
|
+
if (forced) {
|
|
18384
|
+
if (!exists(forced)) throw new Error(`HANOMAN_AGENT_DOC menunjuk berkas yang tak ada: ${forced}`);
|
|
18385
|
+
return forced;
|
|
18386
|
+
}
|
|
18387
|
+
for (const c of [resolve2(distDir, "..", AGENT_DOC_REL), resolve2(distDir, "../..", AGENT_DOC_REL)]) {
|
|
18388
|
+
if (exists(c)) return c;
|
|
18389
|
+
}
|
|
18390
|
+
return null;
|
|
18391
|
+
}
|
|
18392
|
+
|
|
17978
18393
|
// src/routes/health.ts
|
|
17979
18394
|
async function health_default(app2) {
|
|
17980
18395
|
app2.get("/health", async () => ({ ok: true }));
|
|
17981
18396
|
}
|
|
17982
18397
|
|
|
18398
|
+
// src/routes/agent-doc.ts
|
|
18399
|
+
import { readFile } from "node:fs/promises";
|
|
18400
|
+
async function agent_doc_default(app2, opts) {
|
|
18401
|
+
app2.get("/agent-integration.md", async (_req, reply) => {
|
|
18402
|
+
if (!opts.file)
|
|
18403
|
+
return reply.code(404).send({
|
|
18404
|
+
error: `dokumen panduan tak ada di instalasi ini (${AGENT_DOC_REL}) \u2014 pasang ulang paket hanoman atau set HANOMAN_AGENT_DOC`
|
|
18405
|
+
});
|
|
18406
|
+
const text = await readFile(opts.file, "utf8");
|
|
18407
|
+
return reply.type("text/markdown; charset=utf-8").send(text);
|
|
18408
|
+
});
|
|
18409
|
+
}
|
|
18410
|
+
|
|
17983
18411
|
// src/routes/projects.ts
|
|
17984
18412
|
init_src();
|
|
17985
18413
|
init_db();
|
|
@@ -17994,7 +18422,7 @@ init_src();
|
|
|
17994
18422
|
import { execFile } from "node:child_process";
|
|
17995
18423
|
import { promisify } from "node:util";
|
|
17996
18424
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync3, rmSync as rmSync2, mkdirSync as mkdirSync5 } from "node:fs";
|
|
17997
|
-
import { resolve as
|
|
18425
|
+
import { resolve as resolve7, dirname as dirname6, sep } from "node:path";
|
|
17998
18426
|
var exec = promisify(execFile);
|
|
17999
18427
|
async function listRepoDocs(repoDir) {
|
|
18000
18428
|
try {
|
|
@@ -18010,7 +18438,7 @@ async function listRepoDocs(repoDir) {
|
|
|
18010
18438
|
}
|
|
18011
18439
|
function docsDirOf(repoDir) {
|
|
18012
18440
|
try {
|
|
18013
|
-
const raw = readFileSync4(
|
|
18441
|
+
const raw = readFileSync4(resolve7(repoDir, "hanoman.config.json"), "utf8");
|
|
18014
18442
|
return zHanomanConfig.parse(JSON.parse(raw)).docsDir;
|
|
18015
18443
|
} catch {
|
|
18016
18444
|
return zHanomanConfig.parse({}).docsDir;
|
|
@@ -18018,7 +18446,7 @@ function docsDirOf(repoDir) {
|
|
|
18018
18446
|
}
|
|
18019
18447
|
function resolveIndex(repoDir, docsDir) {
|
|
18020
18448
|
const rel = `${docsDir}/README.md`;
|
|
18021
|
-
return existsSync3(
|
|
18449
|
+
return existsSync3(resolve7(repoDir, rel)) ? rel : "";
|
|
18022
18450
|
}
|
|
18023
18451
|
var catOf = (rel) => rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : ".";
|
|
18024
18452
|
var nameOf = (rel) => rel.includes("/") ? rel.slice(rel.lastIndexOf("/") + 1) : rel;
|
|
@@ -18029,7 +18457,7 @@ async function scanRepoDocs(repoDir) {
|
|
|
18029
18457
|
const index = resolveIndex(repoDir, docsDir);
|
|
18030
18458
|
const read = (rel) => {
|
|
18031
18459
|
try {
|
|
18032
|
-
return readFileSync4(
|
|
18460
|
+
return readFileSync4(resolve7(repoDir, rel), "utf8");
|
|
18033
18461
|
} catch {
|
|
18034
18462
|
return null;
|
|
18035
18463
|
}
|
|
@@ -18052,7 +18480,7 @@ async function scanRepoDocs(repoDir) {
|
|
|
18052
18480
|
function docAbsPath(repoDir, rel) {
|
|
18053
18481
|
if (!rel.endsWith(".md")) throw new Error("hanya file .md yang diizinkan");
|
|
18054
18482
|
if (rel.split("/").includes(".git")) throw new Error("tidak boleh menyentuh .git");
|
|
18055
|
-
const abs =
|
|
18483
|
+
const abs = resolve7(repoDir, rel);
|
|
18056
18484
|
if (abs !== repoDir && !abs.startsWith(repoDir + sep)) throw new Error("path keluar dari repo");
|
|
18057
18485
|
return abs;
|
|
18058
18486
|
}
|
|
@@ -18552,9 +18980,9 @@ init_db();
|
|
|
18552
18980
|
// src/services/spec-review.ts
|
|
18553
18981
|
import { execFile as execFile4 } from "node:child_process";
|
|
18554
18982
|
import { promisify as promisify4 } from "node:util";
|
|
18555
|
-
import { readFile, mkdtemp, copyFile, rm } from "node:fs/promises";
|
|
18983
|
+
import { readFile as readFile2, mkdtemp, copyFile, rm } from "node:fs/promises";
|
|
18556
18984
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
18557
|
-
import { join as join4, resolve as
|
|
18985
|
+
import { join as join4, resolve as resolve8 } from "node:path";
|
|
18558
18986
|
var exec4 = promisify4(execFile4);
|
|
18559
18987
|
var GIT3 = { maxBuffer: 1 << 24 };
|
|
18560
18988
|
var MAX = 256 * 1024;
|
|
@@ -18563,7 +18991,7 @@ async function withTempIndex(wt, fn) {
|
|
|
18563
18991
|
const idx = (await exec4("git", ["rev-parse", "--git-path", "index"], { cwd: wt, ...GIT3 })).stdout.trim();
|
|
18564
18992
|
const dir2 = await mkdtemp(join4(tmpdir2(), "hanoman-idx-"));
|
|
18565
18993
|
const tmp = join4(dir2, "index");
|
|
18566
|
-
await copyFile(
|
|
18994
|
+
await copyFile(resolve8(wt, idx), tmp);
|
|
18567
18995
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
18568
18996
|
try {
|
|
18569
18997
|
await exec4("git", ["add", "-A", "-N"], { cwd: wt, env, ...GIT3 });
|
|
@@ -18687,7 +19115,7 @@ async function reviewFile(repoDir, specId, baseSha, branchFrom, path) {
|
|
|
18687
19115
|
let contentRaw = null;
|
|
18688
19116
|
if (status !== "D") {
|
|
18689
19117
|
try {
|
|
18690
|
-
contentRaw = await
|
|
19118
|
+
contentRaw = await readFile2(join4(wt, path), "utf8");
|
|
18691
19119
|
} catch {
|
|
18692
19120
|
contentRaw = null;
|
|
18693
19121
|
}
|
|
@@ -21300,7 +21728,7 @@ function renderDocPdf(text, name2, meta) {
|
|
|
21300
21728
|
});
|
|
21301
21729
|
const chunks = [];
|
|
21302
21730
|
doc.on("data", (c) => chunks.push(c));
|
|
21303
|
-
const done = new Promise((
|
|
21731
|
+
const done = new Promise((resolve15) => doc.on("end", () => resolve15(Buffer.concat(chunks))));
|
|
21304
21732
|
doc.font("Helvetica-Bold").fontSize(8).fillColor(BRASS).text(toWinAnsi(meta.eyebrow.toUpperCase()), { characterSpacing: 0.8 });
|
|
21305
21733
|
doc.moveDown(0.25);
|
|
21306
21734
|
doc.font("Helvetica-Bold").fontSize(17).fillColor(STRONG).text(toWinAnsi(name2.split("/").pop() ?? name2));
|
|
@@ -21782,6 +22210,7 @@ init_src();
|
|
|
21782
22210
|
init_db();
|
|
21783
22211
|
init_settings2();
|
|
21784
22212
|
init_bootstrap();
|
|
22213
|
+
init_config3();
|
|
21785
22214
|
async function settings_default(app2) {
|
|
21786
22215
|
app2.get("/settings", async () => getSetting());
|
|
21787
22216
|
app2.put("/settings", async (req, reply) => {
|
|
@@ -21793,7 +22222,7 @@ async function settings_default(app2) {
|
|
|
21793
22222
|
update: { data: parsed.data },
|
|
21794
22223
|
create: { id: 1, data: parsed.data }
|
|
21795
22224
|
});
|
|
21796
|
-
if (
|
|
22225
|
+
if (telegramReloadNeeded(before.telegram, parsed.data.telegram)) {
|
|
21797
22226
|
await reloadTelegramGateway();
|
|
21798
22227
|
}
|
|
21799
22228
|
return row.data;
|
|
@@ -22009,16 +22438,16 @@ import { promisify as promisify7 } from "node:util";
|
|
|
22009
22438
|
// src/services/git-ide.ts
|
|
22010
22439
|
import { execFile as execFile6 } from "node:child_process";
|
|
22011
22440
|
import { promisify as promisify6 } from "node:util";
|
|
22012
|
-
import { readFile as
|
|
22441
|
+
import { readFile as readFile3, writeFile, mkdir } from "node:fs/promises";
|
|
22013
22442
|
import { existsSync as existsSync6 } from "node:fs";
|
|
22014
|
-
import { resolve as
|
|
22443
|
+
import { resolve as resolve9, sep as sep2, dirname as dirname8 } from "node:path";
|
|
22015
22444
|
var exec6 = promisify6(execFile6);
|
|
22016
22445
|
var GIT5 = { maxBuffer: 1 << 24 };
|
|
22017
22446
|
var MAX2 = 256 * 1024;
|
|
22018
22447
|
var splitZ2 = (s2) => s2.split("\0").filter(Boolean);
|
|
22019
22448
|
function repoAbsPath(repoDir, rel) {
|
|
22020
22449
|
if (rel.split(/[\\/]/).includes(".git")) throw new Error("tidak boleh menyentuh .git");
|
|
22021
|
-
const abs =
|
|
22450
|
+
const abs = resolve9(repoDir, rel);
|
|
22022
22451
|
if (abs !== repoDir && !abs.startsWith(repoDir + sep2)) throw new Error("path keluar dari repo");
|
|
22023
22452
|
return abs;
|
|
22024
22453
|
}
|
|
@@ -22036,7 +22465,7 @@ async function readRepoFile(repoDir, rel, ref = "") {
|
|
|
22036
22465
|
repoAbsPath(repoDir, rel);
|
|
22037
22466
|
let raw;
|
|
22038
22467
|
try {
|
|
22039
|
-
raw = ref ? (await exec6("git", ["show", `${ref}:${rel}`], { cwd: repoDir, ...GIT5 })).stdout : await
|
|
22468
|
+
raw = ref ? (await exec6("git", ["show", `${ref}:${rel}`], { cwd: repoDir, ...GIT5 })).stdout : await readFile3(repoAbsPath(repoDir, rel), "utf8");
|
|
22040
22469
|
} catch {
|
|
22041
22470
|
return null;
|
|
22042
22471
|
}
|
|
@@ -22437,7 +22866,7 @@ async function workingFileDiff(repoDir, path, staged) {
|
|
|
22437
22866
|
let contentRaw = null;
|
|
22438
22867
|
if (cf.status !== "D") {
|
|
22439
22868
|
try {
|
|
22440
|
-
contentRaw = staged ? (await exec6("git", ["show", `:${path}`], { cwd: repoDir, ...GIT5 })).stdout : await
|
|
22869
|
+
contentRaw = staged ? (await exec6("git", ["show", `:${path}`], { cwd: repoDir, ...GIT5 })).stdout : await readFile3(repoAbsPath(repoDir, path), "utf8");
|
|
22441
22870
|
} catch {
|
|
22442
22871
|
contentRaw = null;
|
|
22443
22872
|
}
|
|
@@ -22903,11 +23332,11 @@ async function finishGraphOp(reply, id, repoDir, r, verb) {
|
|
|
22903
23332
|
// src/routes/fs.ts
|
|
22904
23333
|
import { readdir } from "node:fs/promises";
|
|
22905
23334
|
import { homedir as homedir3 } from "node:os";
|
|
22906
|
-
import { resolve as
|
|
23335
|
+
import { resolve as resolve10, dirname as dirname9, join as join5 } from "node:path";
|
|
22907
23336
|
async function fs_default(app2) {
|
|
22908
23337
|
app2.get("/fs/browse", async (req, reply) => {
|
|
22909
23338
|
const q = req.query.path;
|
|
22910
|
-
const dir2 = q && q.trim() ?
|
|
23339
|
+
const dir2 = q && q.trim() ? resolve10(q.trim()) : homedir3();
|
|
22911
23340
|
try {
|
|
22912
23341
|
const ents = await readdir(dir2, { withFileTypes: true });
|
|
22913
23342
|
const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join5(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -23043,11 +23472,11 @@ async function startSpecSession(spec, opts) {
|
|
|
23043
23472
|
}
|
|
23044
23473
|
|
|
23045
23474
|
// src/services/session-worktree.ts
|
|
23046
|
-
import { resolve as
|
|
23475
|
+
import { resolve as resolve11, sep as sep3 } from "node:path";
|
|
23047
23476
|
function ownsWorktree(repoDir, cwd) {
|
|
23048
|
-
const base2 =
|
|
23049
|
-
const dir2 =
|
|
23050
|
-
const container =
|
|
23477
|
+
const base2 = resolve11(repoDir);
|
|
23478
|
+
const dir2 = resolve11(cwd);
|
|
23479
|
+
const container = resolve11(base2, ".worktrees");
|
|
23051
23480
|
return dir2 !== base2 && dir2 !== container && dir2.startsWith(container + sep3);
|
|
23052
23481
|
}
|
|
23053
23482
|
|
|
@@ -23548,14 +23977,14 @@ function sshExec(t, remoteCmd, opts = {}) {
|
|
|
23548
23977
|
SSH_ASKPASS_REQUIRE: "force",
|
|
23549
23978
|
HANOMAN_SSH_PASSWORD: opts.password
|
|
23550
23979
|
} : process.env;
|
|
23551
|
-
return new Promise((
|
|
23980
|
+
return new Promise((resolve15) => {
|
|
23552
23981
|
const p = spawn3(sshBin(), args, { stdio: ["pipe", "pipe", "pipe"], env });
|
|
23553
23982
|
let out3 = "";
|
|
23554
23983
|
const timer7 = setTimeout(() => p.kill("SIGKILL"), opts.timeoutMs ?? 6e4);
|
|
23555
23984
|
const done = (r) => {
|
|
23556
23985
|
clearTimeout(timer7);
|
|
23557
23986
|
if (askpass) rmSync4(dirname10(askpass), { recursive: true, force: true });
|
|
23558
|
-
|
|
23987
|
+
resolve15(r);
|
|
23559
23988
|
};
|
|
23560
23989
|
p.stdout.on("data", (d) => {
|
|
23561
23990
|
out3 += d;
|
|
@@ -24332,7 +24761,7 @@ init_src();
|
|
|
24332
24761
|
init_src();
|
|
24333
24762
|
init_config2();
|
|
24334
24763
|
import { readFileSync as readFileSync11 } from "node:fs";
|
|
24335
|
-
import { resolve as
|
|
24764
|
+
import { resolve as resolve12, dirname as dirname12 } from "node:path";
|
|
24336
24765
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
24337
24766
|
var UPDATE_COMMAND = "npm i -g hanoman@latest";
|
|
24338
24767
|
function composeUpdate(x) {
|
|
@@ -24358,7 +24787,7 @@ var lastLatest = null;
|
|
|
24358
24787
|
var lastStatus = "unavailable";
|
|
24359
24788
|
function runningVersion() {
|
|
24360
24789
|
const here = dirname12(fileURLToPath3(import.meta.url));
|
|
24361
|
-
for (const p of [
|
|
24790
|
+
for (const p of [resolve12(here, "build-info.json"), resolve12(here, "../package.json")]) {
|
|
24362
24791
|
try {
|
|
24363
24792
|
const v = JSON.parse(readFileSync11(p, "utf8")).version;
|
|
24364
24793
|
if (typeof v === "string" && v) return v;
|
|
@@ -24707,8 +25136,8 @@ init_conflicts();
|
|
|
24707
25136
|
// src/services/uploads.ts
|
|
24708
25137
|
init_config2();
|
|
24709
25138
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
24710
|
-
import { mkdir as mkdir2, writeFile as writeFile2, readFile as
|
|
24711
|
-
import { join as join12, resolve as
|
|
25139
|
+
import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile4, unlink } from "node:fs/promises";
|
|
25140
|
+
import { join as join12, resolve as resolve13 } from "node:path";
|
|
24712
25141
|
var EXT = {
|
|
24713
25142
|
"image/png": ".png",
|
|
24714
25143
|
"image/jpeg": ".jpg",
|
|
@@ -24718,7 +25147,7 @@ function extFor(mimeType) {
|
|
|
24718
25147
|
return EXT[mimeType] ?? ".bin";
|
|
24719
25148
|
}
|
|
24720
25149
|
function uploadDir() {
|
|
24721
|
-
return
|
|
25150
|
+
return resolve13(effectiveStr("HANOMAN_UPLOAD_DIR") ?? join12(process.cwd(), "data", "uploads"));
|
|
24722
25151
|
}
|
|
24723
25152
|
async function saveUpload(buf, mimeType) {
|
|
24724
25153
|
const dir2 = uploadDir();
|
|
@@ -24729,13 +25158,13 @@ async function saveUpload(buf, mimeType) {
|
|
|
24729
25158
|
}
|
|
24730
25159
|
async function readUpload(storageKey) {
|
|
24731
25160
|
const safe = storageKey.replace(/[/\\]/g, "");
|
|
24732
|
-
return
|
|
25161
|
+
return readFile4(join12(uploadDir(), safe));
|
|
24733
25162
|
}
|
|
24734
25163
|
async function readUploadOrFetch(storageKey) {
|
|
24735
25164
|
const safe = storageKey.replace(/[/\\]/g, "");
|
|
24736
25165
|
const target2 = join12(uploadDir(), safe);
|
|
24737
25166
|
try {
|
|
24738
|
-
return await
|
|
25167
|
+
return await readFile4(target2);
|
|
24739
25168
|
} catch {
|
|
24740
25169
|
const base2 = effectiveStr("SYNC_SERVER_URL");
|
|
24741
25170
|
const token = effectiveStr("SYNC_DEVICE_TOKEN");
|
|
@@ -24882,11 +25311,11 @@ import { randomUUID as randomUUID5 } from "node:crypto";
|
|
|
24882
25311
|
// src/services/transcript-store.ts
|
|
24883
25312
|
init_config2();
|
|
24884
25313
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
24885
|
-
import { mkdir as mkdir3, writeFile as writeFile3, readFile as
|
|
24886
|
-
import { join as join13, resolve as
|
|
25314
|
+
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile5, unlink as unlink2 } from "node:fs/promises";
|
|
25315
|
+
import { join as join13, resolve as resolve14, basename as basename2 } from "node:path";
|
|
24887
25316
|
var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
|
|
24888
25317
|
function transcriptDir() {
|
|
24889
|
-
return
|
|
25318
|
+
return resolve14(effectiveStr("HANOMAN_TRANSCRIPT_DIR") ?? join13(process.cwd(), "data", "transcripts"));
|
|
24890
25319
|
}
|
|
24891
25320
|
function clamp(text) {
|
|
24892
25321
|
const buf = Buffer.from(text, "utf8");
|
|
@@ -24910,7 +25339,7 @@ async function saveTranscript(text) {
|
|
|
24910
25339
|
async function readTranscript(key) {
|
|
24911
25340
|
if (!key) return null;
|
|
24912
25341
|
try {
|
|
24913
|
-
return await
|
|
25342
|
+
return await readFile5(join13(transcriptDir(), basename2(key)), "utf8");
|
|
24914
25343
|
} catch {
|
|
24915
25344
|
return null;
|
|
24916
25345
|
}
|
|
@@ -25938,7 +26367,7 @@ function leadFailureReason(agent, timeoutMs, err, stdout, stderr) {
|
|
|
25938
26367
|
function think(prompt, o) {
|
|
25939
26368
|
const bin = binFor(o.agent);
|
|
25940
26369
|
const args = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
|
|
25941
|
-
return new Promise((
|
|
26370
|
+
return new Promise((resolve15, reject2) => {
|
|
25942
26371
|
const child = execFile9(bin, args, {
|
|
25943
26372
|
cwd: o.cwd,
|
|
25944
26373
|
timeout: o.timeoutMs,
|
|
@@ -25951,7 +26380,7 @@ function think(prompt, o) {
|
|
|
25951
26380
|
reject2(new Error(leadFailureReason(o.agent, o.timeoutMs, err, stdout, stderr)));
|
|
25952
26381
|
return;
|
|
25953
26382
|
}
|
|
25954
|
-
|
|
26383
|
+
resolve15(stdout);
|
|
25955
26384
|
});
|
|
25956
26385
|
child.stdin?.end();
|
|
25957
26386
|
});
|
|
@@ -25988,9 +26417,9 @@ function acquire(cap, waitMs) {
|
|
|
25988
26417
|
return Promise.resolve();
|
|
25989
26418
|
}
|
|
25990
26419
|
const startedAt = Date.now();
|
|
25991
|
-
return new Promise((
|
|
26420
|
+
return new Promise((resolve15, reject2) => {
|
|
25992
26421
|
const w = {
|
|
25993
|
-
grant:
|
|
26422
|
+
grant: resolve15,
|
|
25994
26423
|
deny: reject2,
|
|
25995
26424
|
timer: setTimeout(() => {
|
|
25996
26425
|
const i = queue.indexOf(w);
|
|
@@ -26507,6 +26936,7 @@ var CODEX_FINISHED = [
|
|
|
26507
26936
|
/\btokens used\b/i,
|
|
26508
26937
|
/\bTo continue this session\b/i
|
|
26509
26938
|
];
|
|
26939
|
+
var AGENT_TURN_LINE = /(?:\bfor\s+\d+\s*[hms]\b|\(\s*\d+\s*[hms][^)]*·)/i;
|
|
26510
26940
|
var ASK_SIGNALS = [
|
|
26511
26941
|
/\?\s*$/m,
|
|
26512
26942
|
/^\s*\[?\d+[).\]]\s+\S/m,
|
|
@@ -26515,9 +26945,12 @@ var ASK_SIGNALS = [
|
|
|
26515
26945
|
function readPaneQuestion(text, agent) {
|
|
26516
26946
|
const lines2 = tail2(text, 40);
|
|
26517
26947
|
const body = lines2.join("\n").trim();
|
|
26518
|
-
const
|
|
26948
|
+
const dialog = readChoiceDialog(text);
|
|
26949
|
+
const choices = dialog?.options ?? [];
|
|
26519
26950
|
if (!body) return { asking: false, question: "", reason: "layar kosong", choices };
|
|
26520
26951
|
const question = tail2(text, 25).join("\n").trim();
|
|
26952
|
+
if (!dialog && AGENT_TURN_LINE.test(body))
|
|
26953
|
+
return { asking: false, question, reason: "giliran agen, bukan pertanyaan (baris giliran claude di layar)", choices };
|
|
26521
26954
|
if (agent === "codex") {
|
|
26522
26955
|
const finished = CODEX_FINISHED.find((re) => re.test(body));
|
|
26523
26956
|
if (finished) return { asking: false, question, reason: "sesi codex selesai wajar (ADR-0074)", choices };
|
|
@@ -26532,14 +26965,20 @@ var answers = /* @__PURE__ */ new Map();
|
|
|
26532
26965
|
var capped = /* @__PURE__ */ new Set();
|
|
26533
26966
|
var failures = /* @__PURE__ */ new Map();
|
|
26534
26967
|
var failCapped = /* @__PURE__ */ new Set();
|
|
26968
|
+
var failedAt = /* @__PURE__ */ new Map();
|
|
26969
|
+
var FAIL_COOLDOWN_MS = 15 * 6e4;
|
|
26970
|
+
var chainFlows = /* @__PURE__ */ new Map();
|
|
26535
26971
|
var MAX_CHAIN_STEPS = 6;
|
|
26536
26972
|
var CHAIN_POLL_MS = 300;
|
|
26537
26973
|
var CHAIN_POLL_TRIES = 20;
|
|
26974
|
+
var CHAIN_END_TRIES = 5;
|
|
26538
26975
|
function resetSession(sessionId2) {
|
|
26539
26976
|
answers.delete(sessionId2);
|
|
26540
26977
|
capped.delete(sessionId2);
|
|
26541
26978
|
failures.delete(sessionId2);
|
|
26542
26979
|
failCapped.delete(sessionId2);
|
|
26980
|
+
failedAt.delete(sessionId2);
|
|
26981
|
+
chainFlows.delete(sessionId2);
|
|
26543
26982
|
}
|
|
26544
26983
|
var prodDetectDeps = {
|
|
26545
26984
|
live: () => {
|
|
@@ -26577,6 +27016,12 @@ var prodDetectDeps = {
|
|
|
26577
27016
|
},
|
|
26578
27017
|
submit: (id) => submitPaneDialog(id),
|
|
26579
27018
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
27019
|
+
// Alur yang gagal ditutup bukan alasan menggagalkan rantai yang sudah berhasil: penyapu TTL
|
|
27020
|
+
// (ADR-0102) tetap menutupnya.
|
|
27021
|
+
closeChain: async (flowId) => {
|
|
27022
|
+
await closeFlow(flowId, "submit").catch(() => null);
|
|
27023
|
+
},
|
|
27024
|
+
now: Date.now,
|
|
26580
27025
|
decide,
|
|
26581
27026
|
decideDeps: prodDecideDeps,
|
|
26582
27027
|
delivery: takeDelivery,
|
|
@@ -26602,7 +27047,7 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
26602
27047
|
skip("lead dijeda untuk project ini");
|
|
26603
27048
|
continue;
|
|
26604
27049
|
}
|
|
26605
|
-
if (!deps.filled(s2.decisionFile)) continue;
|
|
27050
|
+
if (!deps.filled(s2.decisionFile) && !(!deps.exited(s2.id) && readDialogScreen(deps.pane(s2.id)))) continue;
|
|
26606
27051
|
if (deps.exited(s2.id)) {
|
|
26607
27052
|
skip("pane mati");
|
|
26608
27053
|
continue;
|
|
@@ -26635,6 +27080,12 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
26635
27080
|
skip("batas jawaban otomatis tercapai");
|
|
26636
27081
|
continue;
|
|
26637
27082
|
}
|
|
27083
|
+
const lastFail = failedAt.get(s2.id);
|
|
27084
|
+
if (lastFail !== void 0 && deps.now() - lastFail > FAIL_COOLDOWN_MS) {
|
|
27085
|
+
failures.delete(s2.id);
|
|
27086
|
+
failCapped.delete(s2.id);
|
|
27087
|
+
failedAt.delete(s2.id);
|
|
27088
|
+
}
|
|
26638
27089
|
if ((failures.get(s2.id) ?? 0) >= cfg.maxAutoAnswers) {
|
|
26639
27090
|
if (!failCapped.has(s2.id)) {
|
|
26640
27091
|
failCapped.add(s2.id);
|
|
@@ -26680,15 +27131,27 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
26680
27131
|
out3.answered.push(s2.id);
|
|
26681
27132
|
return;
|
|
26682
27133
|
}
|
|
26683
|
-
if (chain.failed)
|
|
27134
|
+
if (chain.failed) {
|
|
27135
|
+
failures.set(s2.id, (failures.get(s2.id) ?? 0) + 1);
|
|
27136
|
+
failedAt.set(s2.id, deps.now());
|
|
27137
|
+
}
|
|
26684
27138
|
out3.skipped.push({ id: s2.id, reason: chain.reason });
|
|
26685
27139
|
});
|
|
26686
27140
|
return out3;
|
|
26687
27141
|
}
|
|
26688
27142
|
async function runChain(s2, agent, deps) {
|
|
26689
27143
|
let acted = false;
|
|
27144
|
+
const done = async (r) => {
|
|
27145
|
+
const flowId = chainFlows.get(s2.id);
|
|
27146
|
+
if (flowId) {
|
|
27147
|
+
chainFlows.delete(s2.id);
|
|
27148
|
+
await deps.closeChain(flowId).catch(() => {
|
|
27149
|
+
});
|
|
27150
|
+
}
|
|
27151
|
+
return r;
|
|
27152
|
+
};
|
|
26690
27153
|
for (let step = 0; step < MAX_CHAIN_STEPS; step++) {
|
|
26691
|
-
const text = deps.pane(s2.id);
|
|
27154
|
+
const text = step === 0 ? deps.pane(s2.id) : await settledPane(s2.id, deps);
|
|
26692
27155
|
const screen = readDialogScreen(text);
|
|
26693
27156
|
if (screen?.kind === "review") {
|
|
26694
27157
|
if (!await deps.submit(s2.id))
|
|
@@ -26696,10 +27159,10 @@ async function runChain(s2, agent, deps) {
|
|
|
26696
27159
|
acted = true;
|
|
26697
27160
|
continue;
|
|
26698
27161
|
}
|
|
26699
|
-
if (step > 0 && !screen) return { acted, done: true, failed: false, reason: "" };
|
|
27162
|
+
if (step > 0 && !screen) return done({ acted, done: true, failed: false, reason: "" });
|
|
26700
27163
|
const read = readPaneQuestion(text, agent);
|
|
26701
27164
|
if (!read.asking) {
|
|
26702
|
-
return step === 0 ? { acted, done: false, failed: false, reason: read.reason } : { acted, done: true, failed: false, reason: "" };
|
|
27165
|
+
return step === 0 ? { acted, done: false, failed: false, reason: read.reason } : done({ acted, done: true, failed: false, reason: "" });
|
|
26703
27166
|
}
|
|
26704
27167
|
const notes = [`Sesi ini menunggu di terminal; teks di bawah adalah layar terakhirnya. Jawablah sebagai masukan yang bisa langsung diketik ke terminal itu (isi \`reply\`).`];
|
|
26705
27168
|
if (read.choices.length) {
|
|
@@ -26711,22 +27174,32 @@ async function runChain(s2, agent, deps) {
|
|
|
26711
27174
|
`Dialog ini BERANTAI: ${screen.tabs.length} pertanyaan dalam satu tanya (${screen.tabs.map((t) => `${t.answered ? "sudah" : "belum"}: ${t.header}`).join(", ")}). Yang sedang tampil pertanyaan ke-${at + 1}; jawab HANYA pertanyaan itu \u2014 sisanya akan ditanyakan sesudah ini.`
|
|
26712
27175
|
);
|
|
26713
27176
|
}
|
|
27177
|
+
const ask = {
|
|
27178
|
+
projectId: s2.projectId,
|
|
27179
|
+
specId: s2.specId,
|
|
27180
|
+
sessionId: s2.id,
|
|
27181
|
+
gate: "detected",
|
|
27182
|
+
kind: "answer",
|
|
27183
|
+
question: read.question,
|
|
27184
|
+
options: read.choices.length ? read.choices : void 0,
|
|
27185
|
+
notes,
|
|
27186
|
+
chain: true
|
|
27187
|
+
};
|
|
26714
27188
|
let row;
|
|
26715
27189
|
try {
|
|
26716
|
-
row = await deps.decide({
|
|
26717
|
-
projectId: s2.projectId,
|
|
26718
|
-
specId: s2.specId,
|
|
26719
|
-
sessionId: s2.id,
|
|
26720
|
-
gate: "detected",
|
|
26721
|
-
kind: "answer",
|
|
26722
|
-
question: read.question,
|
|
26723
|
-
options: read.choices.length ? read.choices : void 0,
|
|
26724
|
-
notes
|
|
26725
|
-
}, deps.decideDeps);
|
|
27190
|
+
row = await deps.decide({ ...ask, flowId: chainFlows.get(s2.id) ?? null }, deps.decideDeps);
|
|
26726
27191
|
} catch (e) {
|
|
26727
27192
|
if (e instanceof LeadBusyError) return { acted, done: false, failed: false, reason: `lead penuh \u2014 ${e.message}` };
|
|
26728
|
-
throw e;
|
|
27193
|
+
if (!(e instanceof LeadFlowClosedError)) throw e;
|
|
27194
|
+
chainFlows.delete(s2.id);
|
|
27195
|
+
try {
|
|
27196
|
+
row = await deps.decide({ ...ask, flowId: null }, deps.decideDeps);
|
|
27197
|
+
} catch (e2) {
|
|
27198
|
+
if (e2 instanceof LeadBusyError) return { acted, done: false, failed: false, reason: `lead penuh \u2014 ${e2.message}` };
|
|
27199
|
+
throw e2;
|
|
27200
|
+
}
|
|
26729
27201
|
}
|
|
27202
|
+
if (row?.flowId) chainFlows.set(s2.id, row.flowId);
|
|
26730
27203
|
if (!row) return { acted, done: false, failed: false, reason: "lead tak menghasilkan keputusan yang berlaku" };
|
|
26731
27204
|
if (row.status !== "berlaku")
|
|
26732
27205
|
return { acted, done: false, failed: true, reason: "lead tak menghasilkan keputusan yang berlaku" };
|
|
@@ -26736,12 +27209,20 @@ async function runChain(s2, agent, deps) {
|
|
|
26736
27209
|
if (!await deps.send(s2.id, reply, picked))
|
|
26737
27210
|
return { acted, done: false, failed: true, reason: "gagal mengetik ke pane" };
|
|
26738
27211
|
acted = true;
|
|
26739
|
-
if (!screen) return { acted, done: true, failed: false, reason: "" };
|
|
27212
|
+
if (!screen) return done({ acted, done: true, failed: false, reason: "" });
|
|
26740
27213
|
if (!await waitScreenChange(s2.id, dialogKey(text), deps))
|
|
26741
27214
|
return { acted, done: false, failed: true, reason: "layar dialog tak berubah sesudah dijawab" };
|
|
26742
27215
|
}
|
|
26743
27216
|
return { acted, done: false, failed: true, reason: "batas langkah rantai dialog tercapai" };
|
|
26744
27217
|
}
|
|
27218
|
+
async function settledPane(id, deps) {
|
|
27219
|
+
let text = deps.pane(id);
|
|
27220
|
+
for (let i = 0; i < CHAIN_END_TRIES && !readDialogScreen(text); i++) {
|
|
27221
|
+
await deps.sleep(CHAIN_POLL_MS);
|
|
27222
|
+
text = deps.pane(id);
|
|
27223
|
+
}
|
|
27224
|
+
return text;
|
|
27225
|
+
}
|
|
26745
27226
|
async function waitScreenChange(id, before, deps) {
|
|
26746
27227
|
for (let i = 0; i < CHAIN_POLL_TRIES; i++) {
|
|
26747
27228
|
await deps.sleep(CHAIN_POLL_MS);
|
|
@@ -26755,6 +27236,8 @@ function sweep(liveIds) {
|
|
|
26755
27236
|
for (const id of [...capped]) if (!live.has(id)) capped.delete(id);
|
|
26756
27237
|
for (const id of [...failures.keys()]) if (!live.has(id)) failures.delete(id);
|
|
26757
27238
|
for (const id of [...failCapped]) if (!live.has(id)) failCapped.delete(id);
|
|
27239
|
+
for (const id of [...failedAt.keys()]) if (!live.has(id)) failedAt.delete(id);
|
|
27240
|
+
for (const id of [...chainFlows.keys()]) if (!live.has(id)) chainFlows.delete(id);
|
|
26758
27241
|
}
|
|
26759
27242
|
|
|
26760
27243
|
// src/services/lead/pulse.ts
|
|
@@ -27662,7 +28145,7 @@ function issuesFromRest(raw) {
|
|
|
27662
28145
|
}
|
|
27663
28146
|
var GH_FIELDS = "number,title,body,author,labels,url,state,createdAt,updatedAt";
|
|
27664
28147
|
var API2 = "https://api.github.com";
|
|
27665
|
-
var defaultRunGh = (args, env) => new Promise((
|
|
28148
|
+
var defaultRunGh = (args, env) => new Promise((resolve15, reject2) => {
|
|
27666
28149
|
execFile10(
|
|
27667
28150
|
args[0],
|
|
27668
28151
|
args.slice(1),
|
|
@@ -27670,7 +28153,7 @@ var defaultRunGh = (args, env) => new Promise((resolve14, reject2) => {
|
|
|
27670
28153
|
(err, stdout, stderr) => {
|
|
27671
28154
|
const e = err;
|
|
27672
28155
|
if (e && (e.code === "ENOENT" || e.code === "EACCES")) return reject2(e);
|
|
27673
|
-
|
|
28156
|
+
resolve15({ code: err ? Number(err.code ?? 1) : 0, stdout, stderr });
|
|
27674
28157
|
}
|
|
27675
28158
|
);
|
|
27676
28159
|
});
|
|
@@ -27989,8 +28472,42 @@ import { randomBytes as randomBytes5 } from "node:crypto";
|
|
|
27989
28472
|
// src/services/telegram/credentials.ts
|
|
27990
28473
|
init_src();
|
|
27991
28474
|
init_config2();
|
|
28475
|
+
init_agent_token();
|
|
28476
|
+
init_settings2();
|
|
28477
|
+
init_bootstrap();
|
|
27992
28478
|
init_client();
|
|
27993
28479
|
init_protocol();
|
|
28480
|
+
init_runtime();
|
|
28481
|
+
async function verifyTelegramAgentToken(token, deps = {}) {
|
|
28482
|
+
const agent = await (deps.verify ?? verifyAgentToken)(token);
|
|
28483
|
+
if (!agent) {
|
|
28484
|
+
return {
|
|
28485
|
+
ok: false,
|
|
28486
|
+
missing: [...TELEGRAM_REQUIRED_CAPABILITIES],
|
|
28487
|
+
reason: "AgentToken tidak dikenal atau sudah dicabut \u2014 salin plaintext `hnm_agt_\u2026` dari Akses AI Agent, bukan hash-nya."
|
|
28488
|
+
};
|
|
28489
|
+
}
|
|
28490
|
+
const missing = TELEGRAM_REQUIRED_CAPABILITIES.filter((c) => !agent.capabilities.includes(c));
|
|
28491
|
+
return missing.length ? { ok: false, missing, reason: `AgentToken kurang ${missing.length} capability.` } : { ok: true };
|
|
28492
|
+
}
|
|
28493
|
+
async function telegramInboundReadiness(deps = {}) {
|
|
28494
|
+
const polling = deps.polling ?? telegramRuntimeStatus().running;
|
|
28495
|
+
const empty = [];
|
|
28496
|
+
const token = deps.agentToken ?? effectiveStr("HANOMAN_TELEGRAM_AGENT_TOKEN")?.trim();
|
|
28497
|
+
if (!token) {
|
|
28498
|
+
return { ok: false, reason: "AgentToken gateway belum diisi.", missingCapabilities: [...TELEGRAM_REQUIRED_CAPABILITIES], polling };
|
|
28499
|
+
}
|
|
28500
|
+
const setting = await (deps.getSetting ?? getSetting)();
|
|
28501
|
+
if (!setting.agentAccessEnabled) {
|
|
28502
|
+
return { ok: false, reason: "Akses agent mati \u2014 nyalakan master switch di Akses AI Agent.", missingCapabilities: empty, polling };
|
|
28503
|
+
}
|
|
28504
|
+
const gate = await verifyTelegramAgentToken(token, deps);
|
|
28505
|
+
if (!gate.ok) return { ok: false, reason: gate.reason, missingCapabilities: gate.missing, polling };
|
|
28506
|
+
if (!polling) {
|
|
28507
|
+
return { ok: false, reason: "Kredensial sudah sah tapi gateway belum polling \u2014 nyalakan \u201CGateway aktif\u201D.", missingCapabilities: empty, polling };
|
|
28508
|
+
}
|
|
28509
|
+
return { ok: true, reason: null, missingCapabilities: empty, polling };
|
|
28510
|
+
}
|
|
27994
28511
|
var KEYS = TELEGRAM_CONFIG_KEYS;
|
|
27995
28512
|
function telegramCredentialView() {
|
|
27996
28513
|
return {
|
|
@@ -28008,7 +28525,7 @@ function telegramCredentialView() {
|
|
|
28008
28525
|
})
|
|
28009
28526
|
};
|
|
28010
28527
|
}
|
|
28011
|
-
async function saveTelegramCredentials(patch) {
|
|
28528
|
+
async function saveTelegramCredentials(patch, deps = {}) {
|
|
28012
28529
|
const writes = [];
|
|
28013
28530
|
for (const [key, raw] of Object.entries(patch)) {
|
|
28014
28531
|
if (!KEYS.includes(key)) return { ok: false, key, error: "key bukan kredensial Telegram" };
|
|
@@ -28022,6 +28539,14 @@ async function saveTelegramCredentials(patch) {
|
|
|
28022
28539
|
if (!parsed.ok) return { ok: false, key, error: parsed.error };
|
|
28023
28540
|
writes.push([key, parsed.value]);
|
|
28024
28541
|
}
|
|
28542
|
+
const agentToken = writes.find(([key]) => key === "HANOMAN_TELEGRAM_AGENT_TOKEN")?.[1];
|
|
28543
|
+
if (agentToken !== void 0) {
|
|
28544
|
+
const gate = await verifyTelegramAgentToken(agentToken, deps);
|
|
28545
|
+
if (!gate.ok) {
|
|
28546
|
+
const detail = gate.missing.length < TELEGRAM_REQUIRED_CAPABILITIES.length ? `${gate.reason} Kurang: ${gate.missing.join(", ")}.` : gate.reason;
|
|
28547
|
+
return { ok: false, key: "HANOMAN_TELEGRAM_AGENT_TOKEN", error: detail };
|
|
28548
|
+
}
|
|
28549
|
+
}
|
|
28025
28550
|
for (const [key, value] of writes) await setConfig(key, value);
|
|
28026
28551
|
return { ok: true };
|
|
28027
28552
|
}
|
|
@@ -28036,11 +28561,12 @@ async function clearTelegramCredentials() {
|
|
|
28036
28561
|
return { cleared, envFallback: KEYS.filter((k) => sourceOf(k) === "env") };
|
|
28037
28562
|
}
|
|
28038
28563
|
async function testTelegramConnection(deps = {}) {
|
|
28564
|
+
const inbound = await telegramInboundReadiness(deps);
|
|
28039
28565
|
const botToken = deps.botToken ?? effectiveStr("HANOMAN_TELEGRAM_BOT_TOKEN");
|
|
28040
|
-
if (!botToken) return { ok: false, error: "Bot token belum diisi." };
|
|
28566
|
+
if (!botToken) return { ok: false, error: "Bot token belum diisi.", inbound };
|
|
28041
28567
|
const chatId = deps.chatId ?? resolveTestChatId();
|
|
28042
28568
|
if (!chatId) {
|
|
28043
|
-
return { ok: false, error: "Isi Chat / Channel ID target \u2014 atau isi allowlist dengan tepat satu user id." };
|
|
28569
|
+
return { ok: false, inbound, error: "Isi Chat / Channel ID target \u2014 atau isi allowlist dengan tepat satu user id." };
|
|
28044
28570
|
}
|
|
28045
28571
|
const timeoutMs = deps.timeoutMs ?? 1e4;
|
|
28046
28572
|
const controller = new AbortController();
|
|
@@ -28052,11 +28578,11 @@ async function testTelegramConnection(deps = {}) {
|
|
|
28052
28578
|
const me = await client.getMe();
|
|
28053
28579
|
const stamp2 = (deps.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
28054
28580
|
await client.sendMessage({ chatId, text: `hanoman: uji koneksi Telegram berhasil (${stamp2}).` });
|
|
28055
|
-
return { ok: true, botUsername: me.username ?? null, chatId };
|
|
28581
|
+
return { ok: true, botUsername: me.username ?? null, chatId, inbound };
|
|
28056
28582
|
} catch (error) {
|
|
28057
28583
|
const raw = error.message || "gagal menghubungi Telegram";
|
|
28058
28584
|
const message = controller.signal.aborted ? `Timeout ${timeoutMs} ms \u2014 Telegram tidak menjawab.` : raw;
|
|
28059
|
-
return { ok: false, error: sanitizeTelegramOutput(message, [botToken]).slice(0, 500) };
|
|
28585
|
+
return { ok: false, inbound, error: sanitizeTelegramOutput(message, [botToken]).slice(0, 500) };
|
|
28060
28586
|
} finally {
|
|
28061
28587
|
clearTimeout(timer7);
|
|
28062
28588
|
}
|
|
@@ -28961,9 +29487,15 @@ var PUBLIC = /* @__PURE__ */ new Set([
|
|
|
28961
29487
|
"GET /api/health",
|
|
28962
29488
|
"GET /api/auth/status",
|
|
28963
29489
|
"POST /api/auth/login",
|
|
28964
|
-
"POST /api/auth/setup"
|
|
29490
|
+
"POST /api/auth/setup",
|
|
29491
|
+
// SPEC-489 · panduan AI agent. Sengaja tanpa auth: byte-nya sudah publik di GitHub, dan
|
|
29492
|
+
// "cukup diberi link + token" hanya benar bila link-nya terbaca SEBELUM token disetel —
|
|
29493
|
+
// menggerbanginya berarti agen yang capability-nya kurang menerima 403 pada dokumen yang
|
|
29494
|
+
// justru menjelaskan arti 403 itu.
|
|
29495
|
+
"GET /api/agent-integration.md"
|
|
28965
29496
|
]);
|
|
28966
|
-
function buildApp({ requireAuth = true } = {}) {
|
|
29497
|
+
function buildApp({ requireAuth = true, agentDocFile } = {}) {
|
|
29498
|
+
const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname13(fileURLToPath4(import.meta.url)), process.env, existsSync12);
|
|
28967
29499
|
const app2 = Fastify({ logger: false, trustProxy: true });
|
|
28968
29500
|
app2.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
|
|
28969
29501
|
if (!body) return done(null, void 0);
|
|
@@ -29017,6 +29549,7 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
29017
29549
|
api.addHook("onResponse", auditTelegramGatewayResponse);
|
|
29018
29550
|
await api.register(auth_default);
|
|
29019
29551
|
await api.register(health_default);
|
|
29552
|
+
await api.register(agent_doc_default, { file: docFile });
|
|
29020
29553
|
await api.register(projects_default);
|
|
29021
29554
|
await api.register(specs_default);
|
|
29022
29555
|
await api.register(notifications_default);
|