hanoman 0.1.9 → 0.1.11
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 +210 -4
- package/dist/server.js +2215 -174
- package/package.json +1 -1
- package/prisma/migrations/20260801170000_github_issue/migration.sql +31 -0
- package/prisma/migrations/20260801180000_telegram_gateway/migration.sql +112 -0
- package/prisma/schema.prisma +143 -0
- package/web/assets/{index-D5MU9hT6.js → index-D8IVSqJP.js} +1551 -1551
- package/web/index.html +1 -1
package/dist/server.js
CHANGED
|
@@ -617,10 +617,10 @@ function isValidJWT(jwt, alg) {
|
|
|
617
617
|
if (!jwtRegex.test(jwt))
|
|
618
618
|
return false;
|
|
619
619
|
try {
|
|
620
|
-
const [
|
|
621
|
-
if (!
|
|
620
|
+
const [header2] = jwt.split(".");
|
|
621
|
+
if (!header2)
|
|
622
622
|
return false;
|
|
623
|
-
const base64 =
|
|
623
|
+
const base64 = header2.replace(/-/g, "+").replace(/_/g, "/").padEnd(header2.length + (4 - header2.length % 4) % 4, "=");
|
|
624
624
|
const decoded = JSON.parse(atob(base64));
|
|
625
625
|
if (typeof decoded !== "object" || decoded === null)
|
|
626
626
|
return false;
|
|
@@ -4164,6 +4164,105 @@ var init_enums = __esm({
|
|
|
4164
4164
|
}
|
|
4165
4165
|
});
|
|
4166
4166
|
|
|
4167
|
+
// ../shared/src/telegram.ts
|
|
4168
|
+
var zTelegramSettings, TELEGRAM_DEFAULTS, zTelegramReadiness, zTelegramGatewayStatus, zTelegramMemoryRecord, zTelegramChatContext, TELEGRAM_REPLY_KINDS, zTelegramReplyKind, zTelegramConfirmationRequest, zTelegramReplyInput, zTelegramAuditRecord, zTerminalSteerInput;
|
|
4169
|
+
var init_telegram = __esm({
|
|
4170
|
+
"../shared/src/telegram.ts"() {
|
|
4171
|
+
"use strict";
|
|
4172
|
+
init_zod();
|
|
4173
|
+
init_enums();
|
|
4174
|
+
zTelegramSettings = external_exports.object({
|
|
4175
|
+
enabled: external_exports.boolean().default(false),
|
|
4176
|
+
progress: external_exports.boolean().default(true)
|
|
4177
|
+
});
|
|
4178
|
+
TELEGRAM_DEFAULTS = zTelegramSettings.parse({});
|
|
4179
|
+
zTelegramReadiness = external_exports.enum([
|
|
4180
|
+
"disabled",
|
|
4181
|
+
"misconfigured",
|
|
4182
|
+
"ready",
|
|
4183
|
+
"running",
|
|
4184
|
+
"error"
|
|
4185
|
+
]);
|
|
4186
|
+
zTelegramGatewayStatus = external_exports.object({
|
|
4187
|
+
configured: external_exports.boolean(),
|
|
4188
|
+
enabled: external_exports.boolean(),
|
|
4189
|
+
running: external_exports.boolean(),
|
|
4190
|
+
readiness: zTelegramReadiness,
|
|
4191
|
+
botUsername: external_exports.string().min(1).nullable().default(null),
|
|
4192
|
+
allowlistCount: external_exports.number().int().min(0),
|
|
4193
|
+
agentTokenConfigured: external_exports.boolean(),
|
|
4194
|
+
missingCapabilities: external_exports.array(external_exports.string()).default([]),
|
|
4195
|
+
lastUpdateAt: external_exports.string().datetime().nullable().default(null),
|
|
4196
|
+
lastError: external_exports.string().nullable().default(null)
|
|
4197
|
+
});
|
|
4198
|
+
zTelegramMemoryRecord = external_exports.object({
|
|
4199
|
+
id: external_exports.string().min(1),
|
|
4200
|
+
chatId: external_exports.string().min(1),
|
|
4201
|
+
content: external_exports.string().trim().min(1).max(1e3),
|
|
4202
|
+
createdAt: external_exports.string().datetime(),
|
|
4203
|
+
updatedAt: external_exports.string().datetime()
|
|
4204
|
+
});
|
|
4205
|
+
zTelegramChatContext = external_exports.object({
|
|
4206
|
+
chatId: external_exports.string().min(1),
|
|
4207
|
+
userId: external_exports.string().min(1),
|
|
4208
|
+
sessionId: external_exports.string().nullable(),
|
|
4209
|
+
activeProjectId: external_exports.string().nullable(),
|
|
4210
|
+
activeSessionId: external_exports.string().nullable(),
|
|
4211
|
+
personalityAgentId: external_exports.string().nullable(),
|
|
4212
|
+
summary: external_exports.string().max(4e3).nullable(),
|
|
4213
|
+
agent: zAgent,
|
|
4214
|
+
model: external_exports.string().min(1),
|
|
4215
|
+
effort: external_exports.string().min(1),
|
|
4216
|
+
memories: external_exports.array(zTelegramMemoryRecord)
|
|
4217
|
+
});
|
|
4218
|
+
TELEGRAM_REPLY_KINDS = [
|
|
4219
|
+
"progress",
|
|
4220
|
+
"final",
|
|
4221
|
+
"decision",
|
|
4222
|
+
"failure",
|
|
4223
|
+
"confirmation"
|
|
4224
|
+
];
|
|
4225
|
+
zTelegramReplyKind = external_exports.enum(TELEGRAM_REPLY_KINDS);
|
|
4226
|
+
zTelegramConfirmationRequest = external_exports.object({
|
|
4227
|
+
description: external_exports.string().trim().min(1).max(500),
|
|
4228
|
+
method: external_exports.enum(["POST", "PATCH", "PUT", "DELETE"]),
|
|
4229
|
+
path: external_exports.string().startsWith("/api/").max(1e3)
|
|
4230
|
+
});
|
|
4231
|
+
zTelegramReplyInput = external_exports.object({
|
|
4232
|
+
chatId: external_exports.string().min(1),
|
|
4233
|
+
updateId: external_exports.number().int().nonnegative(),
|
|
4234
|
+
kind: zTelegramReplyKind,
|
|
4235
|
+
text: external_exports.string().trim().min(1).max(12e3),
|
|
4236
|
+
summary: external_exports.string().trim().min(1).max(4e3).optional(),
|
|
4237
|
+
remember: external_exports.array(external_exports.string().trim().min(1).max(1e3)).max(20).optional(),
|
|
4238
|
+
confirmation: zTelegramConfirmationRequest.optional()
|
|
4239
|
+
}).superRefine((value, ctx) => {
|
|
4240
|
+
if (value.kind === "confirmation" && !value.confirmation) {
|
|
4241
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["confirmation"], message: "confirmation required" });
|
|
4242
|
+
}
|
|
4243
|
+
if (value.kind !== "confirmation" && value.confirmation) {
|
|
4244
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["confirmation"], message: "confirmation only valid for confirmation replies" });
|
|
4245
|
+
}
|
|
4246
|
+
});
|
|
4247
|
+
zTelegramAuditRecord = external_exports.object({
|
|
4248
|
+
id: external_exports.string().min(1),
|
|
4249
|
+
chatId: external_exports.string().nullable(),
|
|
4250
|
+
userId: external_exports.string().nullable(),
|
|
4251
|
+
updateId: external_exports.number().int().nonnegative().nullable(),
|
|
4252
|
+
action: external_exports.string().min(1),
|
|
4253
|
+
outcome: external_exports.string().min(1),
|
|
4254
|
+
correlationId: external_exports.string().nullable(),
|
|
4255
|
+
method: external_exports.string().nullable(),
|
|
4256
|
+
path: external_exports.string().nullable(),
|
|
4257
|
+
statusCode: external_exports.number().int().nullable(),
|
|
4258
|
+
createdAt: external_exports.string().datetime()
|
|
4259
|
+
});
|
|
4260
|
+
zTerminalSteerInput = external_exports.object({
|
|
4261
|
+
text: external_exports.string().trim().min(1).max(16e3)
|
|
4262
|
+
});
|
|
4263
|
+
}
|
|
4264
|
+
});
|
|
4265
|
+
|
|
4167
4266
|
// ../shared/src/entities.ts
|
|
4168
4267
|
function codexModel(id) {
|
|
4169
4268
|
return CODEX_MODELS.find((m) => m.id === id);
|
|
@@ -4188,6 +4287,7 @@ var init_entities = __esm({
|
|
|
4188
4287
|
"use strict";
|
|
4189
4288
|
init_zod();
|
|
4190
4289
|
init_enums();
|
|
4290
|
+
init_telegram();
|
|
4191
4291
|
zProject = external_exports.object({
|
|
4192
4292
|
id: external_exports.string(),
|
|
4193
4293
|
name: external_exports.string(),
|
|
@@ -4376,8 +4476,10 @@ var init_entities = __esm({
|
|
|
4376
4476
|
// SPEC-376 · ADR-0080 · scope verifikasi sesi
|
|
4377
4477
|
conflict: zConflict.default(CONFLICT_DEFAULTS),
|
|
4378
4478
|
// SPEC-383 · ADR-0081 · default sesi konflik rebase/merge
|
|
4379
|
-
lead: zLead.default(LEAD_DEFAULTS)
|
|
4479
|
+
lead: zLead.default(LEAD_DEFAULTS),
|
|
4380
4480
|
// SPEC-409 · ADR-0091 · hanoman-lead (default mati)
|
|
4481
|
+
telegram: zTelegramSettings.default(TELEGRAM_DEFAULTS)
|
|
4482
|
+
// SPEC-476 · ADR-0096 · gateway Telegram (default mati)
|
|
4381
4483
|
});
|
|
4382
4484
|
zNotification = external_exports.object({
|
|
4383
4485
|
id: external_exports.string(),
|
|
@@ -4451,7 +4553,11 @@ var init_agent = __esm({
|
|
|
4451
4553
|
// definisi agen mengubah apa yang dilihat SETIAP sesi baru di seluruh workspace, jadi izin
|
|
4452
4554
|
// baca tak pernah cukup untuk itu (kelas bug SPEC-405).
|
|
4453
4555
|
"agents:read",
|
|
4454
|
-
"agents:write"
|
|
4556
|
+
"agents:write",
|
|
4557
|
+
// SPEC-476 · ADR-0096 · context/memory/reply/audit kanal Telegram. Aksi produk tetap memakai
|
|
4558
|
+
// capability domain produk masing-masing; domain ini tidak memberi akses shell atau sesi.
|
|
4559
|
+
"telegram:read",
|
|
4560
|
+
"telegram:write"
|
|
4455
4561
|
];
|
|
4456
4562
|
zCapability = external_exports.enum(CAPABILITY_IDS);
|
|
4457
4563
|
zCapabilityInfo = external_exports.object({
|
|
@@ -4484,7 +4590,9 @@ var init_agent = __esm({
|
|
|
4484
4590
|
{ id: "lead:read", domain: "lead", access: "read", label: "Lead \u2014 baca", desc: "Baca jejak keputusan hanoman-lead & statusnya." },
|
|
4485
4591
|
{ id: "lead:write", domain: "lead", access: "write", label: "Lead \u2014 tulis", desc: "Minta putusan ke hanoman-lead (keputusan bisa menggerakkan sesi).", risk: "exec" },
|
|
4486
4592
|
{ id: "agents:read", domain: "agents", access: "read", label: "Custom agent \u2014 baca", desc: "Lihat katalog custom agent global & per project." },
|
|
4487
|
-
{ id: "agents:write", domain: "agents", access: "write", label: "Custom agent \u2014 tulis", desc: "Buat/ubah/hapus custom agent; definisinya dipakai setiap sesi baru.", risk: "exec" }
|
|
4593
|
+
{ id: "agents:write", domain: "agents", access: "write", label: "Custom agent \u2014 tulis", desc: "Buat/ubah/hapus custom agent; definisinya dipakai setiap sesi baru.", risk: "exec" },
|
|
4594
|
+
{ id: "telegram:read", domain: "telegram", access: "read", label: "Telegram \u2014 baca", desc: "Baca status gateway, binding, memory, dan audit Telegram." },
|
|
4595
|
+
{ id: "telegram:write", domain: "telegram", access: "write", label: "Telegram \u2014 tulis", desc: "Perbarui context/memory dan terbitkan reply sesi operator Telegram." }
|
|
4488
4596
|
];
|
|
4489
4597
|
zAgentTokenView = external_exports.object({
|
|
4490
4598
|
id: external_exports.string(),
|
|
@@ -4606,6 +4714,47 @@ var init_custom_agent = __esm({
|
|
|
4606
4714
|
}
|
|
4607
4715
|
});
|
|
4608
4716
|
|
|
4717
|
+
// ../shared/src/github.ts
|
|
4718
|
+
function sourceForLabels(labels) {
|
|
4719
|
+
const hay = labels.map((l) => l.toLowerCase());
|
|
4720
|
+
for (const rule2 of LABEL_RULES)
|
|
4721
|
+
if (hay.some((l) => rule2.needles.some((n) => l.includes(n)))) return rule2.source;
|
|
4722
|
+
return "qa";
|
|
4723
|
+
}
|
|
4724
|
+
var zGithubIssueStatus, zNormalIssue, zGithubIssueView, LABEL_RULES;
|
|
4725
|
+
var init_github = __esm({
|
|
4726
|
+
"../shared/src/github.ts"() {
|
|
4727
|
+
"use strict";
|
|
4728
|
+
init_zod();
|
|
4729
|
+
zGithubIssueStatus = external_exports.enum(["new", "accepted", "rejected"]);
|
|
4730
|
+
zNormalIssue = external_exports.object({
|
|
4731
|
+
number: external_exports.number().int().positive(),
|
|
4732
|
+
title: external_exports.string(),
|
|
4733
|
+
body: external_exports.string(),
|
|
4734
|
+
authorLogin: external_exports.string(),
|
|
4735
|
+
labels: external_exports.array(external_exports.string()),
|
|
4736
|
+
url: external_exports.string(),
|
|
4737
|
+
issueState: external_exports.enum(["open", "closed"]),
|
|
4738
|
+
issueCreatedAt: external_exports.string(),
|
|
4739
|
+
// ISO-8601 apa adanya dari GitHub
|
|
4740
|
+
issueUpdatedAt: external_exports.string()
|
|
4741
|
+
});
|
|
4742
|
+
zGithubIssueView = zNormalIssue.extend({
|
|
4743
|
+
id: external_exports.string(),
|
|
4744
|
+
projectId: external_exports.string(),
|
|
4745
|
+
repoSlug: external_exports.string(),
|
|
4746
|
+
status: zGithubIssueStatus,
|
|
4747
|
+
specId: external_exports.string().nullable(),
|
|
4748
|
+
pulledAt: external_exports.string()
|
|
4749
|
+
});
|
|
4750
|
+
LABEL_RULES = [
|
|
4751
|
+
{ needles: ["bug", "defect", "regression"], source: "qa" },
|
|
4752
|
+
{ needles: ["question", "docs", "documentation"], source: "audit" },
|
|
4753
|
+
{ needles: ["enhancement", "feature", "feat"], source: "brief" }
|
|
4754
|
+
];
|
|
4755
|
+
}
|
|
4756
|
+
});
|
|
4757
|
+
|
|
4609
4758
|
// ../shared/src/lead.ts
|
|
4610
4759
|
function leadActionAllowed(action) {
|
|
4611
4760
|
return LEAD_ACTIONS.includes(action);
|
|
@@ -5167,6 +5316,8 @@ var init_api = __esm({
|
|
|
5167
5316
|
fsBrowse: (path) => `${API}/fs/browse${path ? `?path=${encodeURIComponent(path)}` : ""}`,
|
|
5168
5317
|
terminalSessions: `${API}/terminal/sessions`,
|
|
5169
5318
|
terminalSession: (id) => `${API}/terminal/sessions/${id}`,
|
|
5319
|
+
terminalSteer: (id) => `${API}/terminal/sessions/${id}/steer`,
|
|
5320
|
+
terminalInterrupt: (id) => `${API}/terminal/sessions/${id}/interrupt`,
|
|
5170
5321
|
terminalPhases: (id) => `${API}/terminal/sessions/${id}/phases`,
|
|
5171
5322
|
// SPEC-230 · review + integrate ber-skop sesi (sesi project-level PRD, tanpa Spec).
|
|
5172
5323
|
sessionReview: (id) => `${API}/terminal/sessions/${id}/review`,
|
|
@@ -5221,6 +5372,13 @@ var init_api = __esm({
|
|
|
5221
5372
|
// SPEC-450 · ADR-0094 · katalog custom agent. `?projectId=` → himpunan EFEKTIF (global+project).
|
|
5222
5373
|
customAgents: `${API}/custom-agents`,
|
|
5223
5374
|
customAgent: (id) => `${API}/custom-agents/${encodeURIComponent(id)}`,
|
|
5375
|
+
// SPEC-476 · ADR-0096 · observability/context/reply kanal Telegram.
|
|
5376
|
+
telegramStatus: `${API}/telegram/status`,
|
|
5377
|
+
telegramContext: (chatId) => `${API}/telegram/chats/${encodeURIComponent(chatId)}/context`,
|
|
5378
|
+
telegramMemories: (chatId) => `${API}/telegram/chats/${encodeURIComponent(chatId)}/memories`,
|
|
5379
|
+
telegramMemory: (chatId, id) => `${API}/telegram/chats/${encodeURIComponent(chatId)}/memories/${encodeURIComponent(id)}`,
|
|
5380
|
+
telegramReplies: `${API}/telegram/replies`,
|
|
5381
|
+
telegramAudit: `${API}/telegram/audit`,
|
|
5224
5382
|
// SPEC-268 · ADR-0066 · pemicu sync manual (cookie-authed)
|
|
5225
5383
|
syncNow: `${API}/sync/now`,
|
|
5226
5384
|
// SPEC-270 · ADR-0067 · antrean konflik rekonsil (cookie-authed)
|
|
@@ -5243,6 +5401,14 @@ var init_api = __esm({
|
|
|
5243
5401
|
ticketUnlink: (id) => `${API}/tickets/${id}/unlink`,
|
|
5244
5402
|
// SPEC-271 · lepas tautan backlog
|
|
5245
5403
|
ticketReject: (id) => `${API}/tickets/${id}/reject`,
|
|
5404
|
+
// SPEC-471 · ADR-0095 · tarik & triase issue GitHub (di belakang gate cookie, capability `support`).
|
|
5405
|
+
// hanoman TIDAK PERNAH menulis ke GitHub — tak ada path komentar/close di sini, dan itu disengaja.
|
|
5406
|
+
githubPull: (id) => `${API}/projects/${encodeURIComponent(id)}/github/pull`,
|
|
5407
|
+
githubIssues: (id) => `${API}/projects/${encodeURIComponent(id)}/github/issues`,
|
|
5408
|
+
githubIssuesAccept: `${API}/github-issues/accept`,
|
|
5409
|
+
githubIssueAccept: (id) => `${API}/github-issues/${encodeURIComponent(id)}/accept`,
|
|
5410
|
+
githubIssueReject: (id) => `${API}/github-issues/${encodeURIComponent(id)}/reject`,
|
|
5411
|
+
githubIssueUnlink: (id) => `${API}/github-issues/${encodeURIComponent(id)}/unlink`,
|
|
5246
5412
|
// SPEC-361 · ADR-0078 · unduh dokumen: query ditempelkan ke URL endpoint dokumen yang sudah ada
|
|
5247
5413
|
// (tak ada endpoint ekspor terpisah). `base` bisa sudah membawa query, mis. ideFile(?path=…).
|
|
5248
5414
|
download: (base, fmt) => `${base}${base.includes("?") ? "&" : "?"}download=${fmt}`
|
|
@@ -5335,7 +5501,8 @@ var init_session_kind = __esm({
|
|
|
5335
5501
|
"vps",
|
|
5336
5502
|
"shell",
|
|
5337
5503
|
"worktree",
|
|
5338
|
-
"terminal"
|
|
5504
|
+
"terminal",
|
|
5505
|
+
"telegram"
|
|
5339
5506
|
];
|
|
5340
5507
|
zSessionKind = external_exports.enum(SESSION_KINDS);
|
|
5341
5508
|
}
|
|
@@ -5450,6 +5617,28 @@ var init_config_registry = __esm({
|
|
|
5450
5617
|
// vps
|
|
5451
5618
|
{ key: "HANOMAN_SSH_KEY_DIR", group: "vps", label: "Dir key SSH", kind: "path", apply: "new-session", category: "knob", help: "Default ~/.hanoman." },
|
|
5452
5619
|
{ key: "HANOMAN_SSH_BIN", group: "vps", label: "Biner ssh", kind: "path", apply: "new-session", category: "knob", default: "ssh" },
|
|
5620
|
+
// github (SPEC-471 · ADR-0095 · tarik issue → backlog). Dua mode auth, satu jalur kode:
|
|
5621
|
+
// `gh` memakai keyring mesin; bila GITHUB_TOKEN diisi ia diteruskan sebagai GH_TOKEN ke
|
|
5622
|
+
// proses gh DAN dipakai jalur REST (hub VPS yang tak punya keyring).
|
|
5623
|
+
{
|
|
5624
|
+
key: "GITHUB_TOKEN",
|
|
5625
|
+
group: "github",
|
|
5626
|
+
label: "GitHub token",
|
|
5627
|
+
kind: "secret",
|
|
5628
|
+
apply: "live",
|
|
5629
|
+
category: "credential",
|
|
5630
|
+
help: "PAT scope `repo` (atau `public_repo`). Kosong = andalkan `gh auth login` di mesin ini."
|
|
5631
|
+
},
|
|
5632
|
+
{
|
|
5633
|
+
key: "HANOMAN_GH_BIN",
|
|
5634
|
+
group: "github",
|
|
5635
|
+
label: "Biner gh",
|
|
5636
|
+
kind: "path",
|
|
5637
|
+
apply: "live",
|
|
5638
|
+
category: "knob",
|
|
5639
|
+
default: "gh",
|
|
5640
|
+
help: "Absen = tarik issue lewat HTTPS langsung ke api.github.com."
|
|
5641
|
+
},
|
|
5453
5642
|
// runtime
|
|
5454
5643
|
{ key: "HANOMAN_EVENTS_TICK_MS", group: "runtime", label: "Interval events (ms)", kind: "int", apply: "live", category: "knob", default: "1000", min: 100 },
|
|
5455
5644
|
{ key: "HANOMAN_UPDATE_FETCH", group: "runtime", label: "Deteksi update saat boot", kind: "bool", apply: "live", category: "knob", default: "1" },
|
|
@@ -5542,6 +5731,7 @@ var init_src = __esm({
|
|
|
5542
5731
|
init_entities();
|
|
5543
5732
|
init_agent();
|
|
5544
5733
|
init_custom_agent();
|
|
5734
|
+
init_github();
|
|
5545
5735
|
init_lead();
|
|
5546
5736
|
init_dto();
|
|
5547
5737
|
init_api();
|
|
@@ -5551,6 +5741,7 @@ var init_src = __esm({
|
|
|
5551
5741
|
init_config();
|
|
5552
5742
|
init_config_registry();
|
|
5553
5743
|
init_semver();
|
|
5744
|
+
init_telegram();
|
|
5554
5745
|
}
|
|
5555
5746
|
});
|
|
5556
5747
|
|
|
@@ -6425,6 +6616,70 @@ var init_paths = __esm({
|
|
|
6425
6616
|
}
|
|
6426
6617
|
});
|
|
6427
6618
|
|
|
6619
|
+
// ../runner/src/telegram-operator.ts
|
|
6620
|
+
function buildTelegramOperatorPrompt(input) {
|
|
6621
|
+
const personality = input.personality ? [
|
|
6622
|
+
`## Personality aktif: @${input.personality.name}`,
|
|
6623
|
+
input.personality.description,
|
|
6624
|
+
input.personality.instructions
|
|
6625
|
+
].join("\n\n") : "## Personality aktif\n\nGunakan fondasi custom agent Hanoman bawaan dan gaya operator yang ringkas.";
|
|
6626
|
+
const summary = input.summary?.trim() || "Belum ada ringkasan percakapan tersimpan.";
|
|
6627
|
+
const memories = input.memories?.length ? input.memories.map((memory) => `- ${memory.id}: ${memory.content}`).join("\n") : "- Belum ada curated memory.";
|
|
6628
|
+
const { update: update2 } = input;
|
|
6629
|
+
return [
|
|
6630
|
+
"# Hanoman \xB7 session operator Telegram persisten",
|
|
6631
|
+
"",
|
|
6632
|
+
"Kamu adalah main session operator Hanoman yang persisten untuk satu private chat Telegram.",
|
|
6633
|
+
"Telegram hanya transport. Jangan membuat runtime agent, loop headless, tool bus, shell executor,",
|
|
6634
|
+
"antrean, atau otak/memory kedua. Semua aksi produk harus memakai API/service Hanoman yang sudah ada.",
|
|
6635
|
+
"Ikuti internal/docs sebagai Source of Truth dan capability/pagar server apa adanya.",
|
|
6636
|
+
"",
|
|
6637
|
+
personality,
|
|
6638
|
+
"",
|
|
6639
|
+
"## Context tahan restart",
|
|
6640
|
+
"",
|
|
6641
|
+
`Ringkasan: ${summary}`,
|
|
6642
|
+
"Curated memory:",
|
|
6643
|
+
memories,
|
|
6644
|
+
"",
|
|
6645
|
+
"## Kontrak kanal",
|
|
6646
|
+
"",
|
|
6647
|
+
"- Bahasa natural adalah antarmuka utama; command hanya shortcut yang tetap kamu tangani di sesi ini.",
|
|
6648
|
+
`- Command minimum: ${COMMANDS.join(", ")}.`,
|
|
6649
|
+
`- Panggil API dengan base $HANOMAN_API_BASE, bearer $HANOMAN_TELEGRAM_AGENT_TOKEN, dan header x-hanoman-telegram-update: ${update2.updateId}.`,
|
|
6650
|
+
"- Terbitkan jawaban user-facing secara eksplisit melalui POST $HANOMAN_API_BASE/api/telegram/replies.",
|
|
6651
|
+
"- Kind reply hanya progress|final|decision|failure|confirmation. Jangan mengandalkan layar PTY sebagai balasan.",
|
|
6652
|
+
"- Aksi sulit dibatalkan: terbitkan reply confirmation, tunggu callback approval, baru panggil endpoint aksinya.",
|
|
6653
|
+
"- Jangan pernah kirim internal reasoning, ANSI, echo command, secret, token, env, credential, atau isi file credential.",
|
|
6654
|
+
"- Curated memory dan summary harus kamu kurasi sendiri lewat API context/memory; jangan simpan transcript mentah.",
|
|
6655
|
+
"",
|
|
6656
|
+
"## Input pertama",
|
|
6657
|
+
"",
|
|
6658
|
+
`[Telegram update ${update2.updateId} \xB7 chat ${update2.chatId} \xB7 kind ${update2.kind}]`,
|
|
6659
|
+
update2.text
|
|
6660
|
+
].join("\n");
|
|
6661
|
+
}
|
|
6662
|
+
var COMMANDS;
|
|
6663
|
+
var init_telegram_operator = __esm({
|
|
6664
|
+
"../runner/src/telegram-operator.ts"() {
|
|
6665
|
+
"use strict";
|
|
6666
|
+
COMMANDS = [
|
|
6667
|
+
"/help",
|
|
6668
|
+
"/status",
|
|
6669
|
+
"/projects",
|
|
6670
|
+
"/project <id>",
|
|
6671
|
+
"/backlog [query]",
|
|
6672
|
+
"/sessions",
|
|
6673
|
+
"/use <session-id>",
|
|
6674
|
+
"/new <brief>",
|
|
6675
|
+
"/stop [session-id]",
|
|
6676
|
+
"/memory [forget <id>|reset]",
|
|
6677
|
+
"/personality [nama|reset]",
|
|
6678
|
+
"/skills"
|
|
6679
|
+
];
|
|
6680
|
+
}
|
|
6681
|
+
});
|
|
6682
|
+
|
|
6428
6683
|
// ../runner/src/index.ts
|
|
6429
6684
|
var init_src2 = __esm({
|
|
6430
6685
|
"../runner/src/index.ts"() {
|
|
@@ -6441,6 +6696,7 @@ var init_src2 = __esm({
|
|
|
6441
6696
|
init_custom_agents();
|
|
6442
6697
|
init_verify_scope();
|
|
6443
6698
|
init_paths();
|
|
6699
|
+
init_telegram_operator();
|
|
6444
6700
|
}
|
|
6445
6701
|
});
|
|
6446
6702
|
|
|
@@ -6568,6 +6824,15 @@ var init_session_phases = __esm({
|
|
|
6568
6824
|
}
|
|
6569
6825
|
});
|
|
6570
6826
|
|
|
6827
|
+
// src/services/session-id.ts
|
|
6828
|
+
var sessionIdForSpec;
|
|
6829
|
+
var init_session_id = __esm({
|
|
6830
|
+
"src/services/session-id.ts"() {
|
|
6831
|
+
"use strict";
|
|
6832
|
+
sessionIdForSpec = (specId) => specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
6833
|
+
}
|
|
6834
|
+
});
|
|
6835
|
+
|
|
6571
6836
|
// src/services/tui-dialog.ts
|
|
6572
6837
|
function readChoiceDialog(paneText2) {
|
|
6573
6838
|
const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
|
|
@@ -6582,7 +6847,7 @@ function readChoiceDialog(paneText2) {
|
|
|
6582
6847
|
const found = [];
|
|
6583
6848
|
for (const line of lines2.slice(0, footer)) {
|
|
6584
6849
|
const m = ROW.exec(line);
|
|
6585
|
-
if (m) found.push({ n: Number(m[1]), label: (m[2] ?? "")
|
|
6850
|
+
if (m) found.push({ n: Number(m[1]), label: cleanLabel(m[2] ?? "") });
|
|
6586
6851
|
}
|
|
6587
6852
|
if (found.length < 2) return null;
|
|
6588
6853
|
const run2 = [];
|
|
@@ -6606,6 +6871,65 @@ function readChoiceDialog(paneText2) {
|
|
|
6606
6871
|
options: rows.filter((r) => !r.free && !r.chat).map((r) => r.label)
|
|
6607
6872
|
};
|
|
6608
6873
|
}
|
|
6874
|
+
function readTabs(lines2) {
|
|
6875
|
+
for (let i = lines2.length - 1; i >= 0; i--) {
|
|
6876
|
+
const line = lines2[i] ?? "";
|
|
6877
|
+
if (!/[☐☒]/.test(line)) continue;
|
|
6878
|
+
const tabs = [];
|
|
6879
|
+
for (const tok of line.split(/\s{2,}/)) {
|
|
6880
|
+
const m = TAB_BOX.exec(tok.trim());
|
|
6881
|
+
if (m) tabs.push({ header: (m[2] ?? "").trim(), answered: m[1] === "\u2612" });
|
|
6882
|
+
}
|
|
6883
|
+
if (tabs.length) return { tabs, at: i };
|
|
6884
|
+
}
|
|
6885
|
+
return { tabs: [], at: -1 };
|
|
6886
|
+
}
|
|
6887
|
+
function readTitle(lines2, stripAt, footer) {
|
|
6888
|
+
if (stripAt < 0) return "";
|
|
6889
|
+
for (let i = stripAt + 1; i < footer; i++) {
|
|
6890
|
+
const line = (lines2[i] ?? "").trim();
|
|
6891
|
+
if (!line || RULE.test(line)) continue;
|
|
6892
|
+
return ROW.test(lines2[i] ?? "") ? "" : line;
|
|
6893
|
+
}
|
|
6894
|
+
return "";
|
|
6895
|
+
}
|
|
6896
|
+
function readReviewScreen(paneText2) {
|
|
6897
|
+
const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
|
|
6898
|
+
const at = lastIndexOf(lines2, REVIEW_PROMPT);
|
|
6899
|
+
if (at < 0) return null;
|
|
6900
|
+
for (const line of lines2.slice(at)) {
|
|
6901
|
+
const m = SUBMIT_ROW.exec(line);
|
|
6902
|
+
if (m) return { submitRow: Number(m[1]) };
|
|
6903
|
+
}
|
|
6904
|
+
return null;
|
|
6905
|
+
}
|
|
6906
|
+
function readDialogScreen(paneText2) {
|
|
6907
|
+
const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
|
|
6908
|
+
const footer = lastIndexOf(lines2, FOOTER);
|
|
6909
|
+
if (lastIndexOf(lines2, REVIEW_PROMPT) > footer) {
|
|
6910
|
+
const review = readReviewScreen(paneText2);
|
|
6911
|
+
if (review) return { kind: "review", submitRow: review.submitRow };
|
|
6912
|
+
}
|
|
6913
|
+
const d = readChoiceDialog(paneText2);
|
|
6914
|
+
if (!d) return null;
|
|
6915
|
+
const { tabs, at } = readTabs(lines2.slice(0, footer));
|
|
6916
|
+
return {
|
|
6917
|
+
kind: "question",
|
|
6918
|
+
rows: d.rows,
|
|
6919
|
+
freeIndex: d.freeIndex,
|
|
6920
|
+
options: d.options,
|
|
6921
|
+
notes: d.freeIndex === null && NOTES_FOOTER.test(lines2[footer] ?? ""),
|
|
6922
|
+
tabs,
|
|
6923
|
+
title: readTitle(lines2, at, footer)
|
|
6924
|
+
};
|
|
6925
|
+
}
|
|
6926
|
+
function dialogKey(paneText2) {
|
|
6927
|
+
const s = readDialogScreen(paneText2);
|
|
6928
|
+
if (!s) return "none";
|
|
6929
|
+
if (s.kind === "review") return "review";
|
|
6930
|
+
const tabs = s.tabs.map((t) => `${t.answered ? "x" : "o"}${t.header}`).join(",");
|
|
6931
|
+
return `q|${tabs}|${s.title || s.options.join("|")}`;
|
|
6932
|
+
}
|
|
6609
6933
|
function freeTextFilled(paneText2, n) {
|
|
6610
6934
|
const row = readChoiceDialog(paneText2)?.rows.find((r) => r.n === n);
|
|
6611
6935
|
if (!row) return false;
|
|
@@ -6623,7 +6947,37 @@ async function answerChoiceDialog(io, freeIndex, line, chunkMs) {
|
|
|
6623
6947
|
io.enter();
|
|
6624
6948
|
return true;
|
|
6625
6949
|
}
|
|
6626
|
-
|
|
6950
|
+
function notesFilled(paneText2) {
|
|
6951
|
+
const lines2 = paneText2.split("\n").map((l) => l.trimEnd());
|
|
6952
|
+
for (let i = lines2.length - 1; i >= 0; i--) {
|
|
6953
|
+
const m = NOTES_LINE.exec(lines2[i] ?? "");
|
|
6954
|
+
if (!m) continue;
|
|
6955
|
+
const v = (m[1] ?? "").trim();
|
|
6956
|
+
return v.length > 0 && !NOTES_PLACEHOLDER.test(v);
|
|
6957
|
+
}
|
|
6958
|
+
return false;
|
|
6959
|
+
}
|
|
6960
|
+
async function answerNotesDialog(io, line, chunkMs) {
|
|
6961
|
+
io.literal("n");
|
|
6962
|
+
await io.sleep(DIALOG_SETTLE_MS);
|
|
6963
|
+
for (const chunk of goalChunks(line)) {
|
|
6964
|
+
io.literal(chunk);
|
|
6965
|
+
await io.sleep(chunkMs);
|
|
6966
|
+
}
|
|
6967
|
+
await io.sleep(DIALOG_SETTLE_MS);
|
|
6968
|
+
if (!notesFilled(io.capture())) return false;
|
|
6969
|
+
io.enter();
|
|
6970
|
+
return true;
|
|
6971
|
+
}
|
|
6972
|
+
async function submitReview(io, submitRow) {
|
|
6973
|
+
io.literal(String(submitRow));
|
|
6974
|
+
for (let i = 0; i < SUBMIT_TRIES; i++) {
|
|
6975
|
+
await io.sleep(DIALOG_SETTLE_MS);
|
|
6976
|
+
if (!readReviewScreen(io.capture())) return true;
|
|
6977
|
+
}
|
|
6978
|
+
return false;
|
|
6979
|
+
}
|
|
6980
|
+
var FOOTER, ROW, PLACEHOLDER, CHAT_ROW, SIDE_PANEL, cleanLabel, REVIEW_PROMPT, SUBMIT_ROW, NOTES_FOOTER, TAB_BOX, lastIndexOf, RULE, DIALOG_SETTLE_MS, NOTES_PLACEHOLDER, NOTES_LINE, SUBMIT_TRIES;
|
|
6627
6981
|
var init_tui_dialog = __esm({
|
|
6628
6982
|
"src/services/tui-dialog.ts"() {
|
|
6629
6983
|
"use strict";
|
|
@@ -6632,7 +6986,21 @@ var init_tui_dialog = __esm({
|
|
|
6632
6986
|
ROW = /^\s*[❯>›]?\s*(\d{1,2})\.\s+(\S.*)$/;
|
|
6633
6987
|
PLACEHOLDER = /^(?:type something\.?|other)$/i;
|
|
6634
6988
|
CHAT_ROW = /^chat about this$/i;
|
|
6989
|
+
SIDE_PANEL = /\s{2,}[│┃┌┐└┘├┤╭╮╰╯─━].*$/;
|
|
6990
|
+
cleanLabel = (s) => s.replace(SIDE_PANEL, "").trim();
|
|
6991
|
+
REVIEW_PROMPT = /^\s*ready to submit your answers\?\s*$/i;
|
|
6992
|
+
SUBMIT_ROW = /^\s*[❯>›]?\s*(\d{1,2})\.\s+submit answers\s*$/i;
|
|
6993
|
+
NOTES_FOOTER = /\bn to add notes\b/i;
|
|
6994
|
+
TAB_BOX = /^([☐☒])\s*(.+)$/;
|
|
6995
|
+
lastIndexOf = (lines2, re) => {
|
|
6996
|
+
for (let i = lines2.length - 1; i >= 0; i--) if (re.test(lines2[i] ?? "")) return i;
|
|
6997
|
+
return -1;
|
|
6998
|
+
};
|
|
6999
|
+
RULE = /^[\s─━╌╍_=]*$/;
|
|
6635
7000
|
DIALOG_SETTLE_MS = 250;
|
|
7001
|
+
NOTES_PLACEHOLDER = /^(?:press n to add notes|add notes on this design(?:…|\.\.\.)?)$/i;
|
|
7002
|
+
NOTES_LINE = /(?:^|\s)Notes:\s*(.*)$/;
|
|
7003
|
+
SUBMIT_TRIES = 8;
|
|
6636
7004
|
}
|
|
6637
7005
|
});
|
|
6638
7006
|
|
|
@@ -6751,6 +7119,7 @@ function registerCustomAgentSource(fn) {
|
|
|
6751
7119
|
function sessionKind(o, projectId, cwd) {
|
|
6752
7120
|
if (o.specId) return "spec";
|
|
6753
7121
|
if (o.flow === "reverse" || o.flow === "prd" || o.flow === "scaffold" || o.flow === "breakdown") return o.flow;
|
|
7122
|
+
if (projectId.startsWith("telegram:")) return "telegram";
|
|
6754
7123
|
if (projectId.startsWith("vps")) return "vps";
|
|
6755
7124
|
if (o.command) return "shell";
|
|
6756
7125
|
if (cwd.includes("/.worktrees/")) return "worktree";
|
|
@@ -6921,18 +7290,13 @@ async function sendToPane(id, text, chunkMs = 50) {
|
|
|
6921
7290
|
const line = text.replace(/\s*\r?\n\s*/g, " ").trim();
|
|
6922
7291
|
if (!line) return false;
|
|
6923
7292
|
try {
|
|
6924
|
-
const io =
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
},
|
|
6932
|
-
sleep
|
|
6933
|
-
};
|
|
6934
|
-
const free = readChoiceDialog(io.capture())?.freeIndex ?? null;
|
|
6935
|
-
if (free !== null) return await answerChoiceDialog(io, free, line, chunkMs);
|
|
7293
|
+
const io = dialogIO(id);
|
|
7294
|
+
const screen = readDialogScreen(io.capture());
|
|
7295
|
+
if (screen?.kind === "review") return await submitReview(io, screen.submitRow);
|
|
7296
|
+
if (screen?.kind === "question") {
|
|
7297
|
+
if (screen.freeIndex !== null) return await answerChoiceDialog(io, screen.freeIndex, line, chunkMs);
|
|
7298
|
+
if (screen.notes) return await answerNotesDialog(io, line, chunkMs);
|
|
7299
|
+
}
|
|
6936
7300
|
for (const chunk of goalChunks(line)) {
|
|
6937
7301
|
io.literal(chunk);
|
|
6938
7302
|
await sleep(chunkMs);
|
|
@@ -6943,6 +7307,28 @@ async function sendToPane(id, text, chunkMs = 50) {
|
|
|
6943
7307
|
return false;
|
|
6944
7308
|
}
|
|
6945
7309
|
}
|
|
7310
|
+
function interruptPane(id) {
|
|
7311
|
+
const pane = getSession(id);
|
|
7312
|
+
if (!pane || pane.exited) return false;
|
|
7313
|
+
try {
|
|
7314
|
+
tmux("send-keys", "-t", name(id), "Escape");
|
|
7315
|
+
return true;
|
|
7316
|
+
} catch {
|
|
7317
|
+
return false;
|
|
7318
|
+
}
|
|
7319
|
+
}
|
|
7320
|
+
async function submitPaneDialog(id) {
|
|
7321
|
+
const p = getSession(id);
|
|
7322
|
+
if (!p || p.exited) return false;
|
|
7323
|
+
try {
|
|
7324
|
+
const io = dialogIO(id);
|
|
7325
|
+
const screen = readDialogScreen(io.capture());
|
|
7326
|
+
if (screen?.kind !== "review") return false;
|
|
7327
|
+
return await submitReview(io, screen.submitRow);
|
|
7328
|
+
} catch {
|
|
7329
|
+
return false;
|
|
7330
|
+
}
|
|
7331
|
+
}
|
|
6946
7332
|
async function armGoalInTui(id, condition, o = {}) {
|
|
6947
7333
|
const pollMs = o.pollMs ?? 500, readyTries = o.readyTries ?? 20;
|
|
6948
7334
|
const settleMs = o.settleMs ?? 1200, verifyTries = o.verifyTries ?? 12;
|
|
@@ -7117,13 +7503,14 @@ function spawnPty(...args) {
|
|
|
7117
7503
|
);
|
|
7118
7504
|
}
|
|
7119
7505
|
}
|
|
7120
|
-
var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, NO_SERVER, TmuxError, sq,
|
|
7506
|
+
var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, NO_SERVER, TmuxError, sq, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText, dialogIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, phaseKey, paneComplete, sessionFinished, poll, detach;
|
|
7121
7507
|
var init_pty = __esm({
|
|
7122
7508
|
"src/services/pty.ts"() {
|
|
7123
7509
|
"use strict";
|
|
7124
7510
|
init_src2();
|
|
7125
7511
|
init_src();
|
|
7126
7512
|
init_session_phases();
|
|
7513
|
+
init_session_id();
|
|
7127
7514
|
init_tui_dialog();
|
|
7128
7515
|
init_config2();
|
|
7129
7516
|
socket = () => effectiveStr("HANOMAN_TMUX_SOCKET") ?? "hanoman";
|
|
@@ -7157,7 +7544,6 @@ var init_pty = __esm({
|
|
|
7157
7544
|
}
|
|
7158
7545
|
};
|
|
7159
7546
|
sq = (s) => `'${s.split("'").join("'\\''")}'`;
|
|
7160
|
-
sessionIdForSpec = (specId) => specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
7161
7547
|
idFor = (specId) => specId ? sessionIdForSpec(specId) : randomUUID().slice(0, 8);
|
|
7162
7548
|
FMT = [
|
|
7163
7549
|
"#{session_name}",
|
|
@@ -7219,6 +7605,16 @@ var init_pty = __esm({
|
|
|
7219
7605
|
return "";
|
|
7220
7606
|
}
|
|
7221
7607
|
};
|
|
7608
|
+
dialogIO = (id) => ({
|
|
7609
|
+
capture: () => capturePane(id, DIALOG_CAPTURE_LINES),
|
|
7610
|
+
literal: (s) => {
|
|
7611
|
+
tmux("send-keys", "-t", name(id), "-l", s);
|
|
7612
|
+
},
|
|
7613
|
+
enter: () => {
|
|
7614
|
+
tmux("send-keys", "-t", name(id), "Enter");
|
|
7615
|
+
},
|
|
7616
|
+
sleep
|
|
7617
|
+
});
|
|
7222
7618
|
DIALOG_CAPTURE_LINES = 60;
|
|
7223
7619
|
GOAL_ARMED_MARKERS = {
|
|
7224
7620
|
claude: ["/goal"],
|
|
@@ -7321,6 +7717,8 @@ var init_rename_project = __esm({
|
|
|
7321
7717
|
var sync_exports = {};
|
|
7322
7718
|
__export(sync_exports, {
|
|
7323
7719
|
SYNCED: () => SYNCED,
|
|
7720
|
+
__DATE_FIELDS: () => __DATE_FIELDS,
|
|
7721
|
+
__FIELDS: () => __FIELDS,
|
|
7324
7722
|
__FIELDS_FOR_TEST: () => __FIELDS_FOR_TEST,
|
|
7325
7723
|
applyPush: () => applyPush,
|
|
7326
7724
|
backfillFeed: () => backfillFeed,
|
|
@@ -7459,13 +7857,13 @@ async function upsertLocal(entity, id, version, data) {
|
|
|
7459
7857
|
function setAcceptedHook(hook) {
|
|
7460
7858
|
onAccepted = hook;
|
|
7461
7859
|
}
|
|
7462
|
-
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
7860
|
+
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, __FIELDS, __DATE_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
7463
7861
|
var init_sync = __esm({
|
|
7464
7862
|
"src/services/sync.ts"() {
|
|
7465
7863
|
"use strict";
|
|
7466
7864
|
init_db();
|
|
7467
7865
|
init_rename_project();
|
|
7468
|
-
SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment", "customAgent"];
|
|
7866
|
+
SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment", "customAgent", "githubIssue"];
|
|
7469
7867
|
DELEGATE = {
|
|
7470
7868
|
project: prisma.project,
|
|
7471
7869
|
spec: prisma.spec,
|
|
@@ -7473,7 +7871,8 @@ var init_sync = __esm({
|
|
|
7473
7871
|
sessionResult: prisma.sessionResult,
|
|
7474
7872
|
ticket: prisma.ticket,
|
|
7475
7873
|
ticketAttachment: prisma.ticketAttachment,
|
|
7476
|
-
customAgent: prisma.customAgent
|
|
7874
|
+
customAgent: prisma.customAgent,
|
|
7875
|
+
githubIssue: prisma.githubIssue
|
|
7477
7876
|
};
|
|
7478
7877
|
FIELDS = {
|
|
7479
7878
|
project: ["name", "desc", "kind", "stack", "gitRemote", "updatedAt"],
|
|
@@ -7493,7 +7892,28 @@ var init_sync = __esm({
|
|
|
7493
7892
|
// ada: `upsert` yang tak menyebut kolom ber-default TETAP berhasil, jadi kolom yang terlewat
|
|
7494
7893
|
// mendarat sebagai default palsu di tiap client tanpa satu pun error (kelas ADR-0090/0093).
|
|
7495
7894
|
// `version` tak pernah masuk FIELDS — ia stempel mekanisme sync itu sendiri.
|
|
7496
|
-
customAgent: ["projectId", "name", "description", "instructions", "tools", "model", "mentions", "enabled", "createdAt", "updatedAt"]
|
|
7895
|
+
customAgent: ["projectId", "name", "description", "instructions", "tools", "model", "mentions", "enabled", "createdAt", "updatedAt"],
|
|
7896
|
+
// SPEC-471 · ADR-0095 · SELURUH kolom bermakna ikut. `status`/`specId` termasuk: keputusan
|
|
7897
|
+
// triase adalah bagian keadaan yang harus dilihat sama oleh semua mesin — tanpa itu satu
|
|
7898
|
+
// mesin bisa menerima ulang issue yang di mesin lain sudah jadi backlog.
|
|
7899
|
+
githubIssue: [
|
|
7900
|
+
"projectId",
|
|
7901
|
+
"repoSlug",
|
|
7902
|
+
"number",
|
|
7903
|
+
"title",
|
|
7904
|
+
"body",
|
|
7905
|
+
"authorLogin",
|
|
7906
|
+
"labels",
|
|
7907
|
+
"url",
|
|
7908
|
+
"issueState",
|
|
7909
|
+
"status",
|
|
7910
|
+
"specId",
|
|
7911
|
+
"issueCreatedAt",
|
|
7912
|
+
"issueUpdatedAt",
|
|
7913
|
+
"pulledAt",
|
|
7914
|
+
"createdAt",
|
|
7915
|
+
"updatedAt"
|
|
7916
|
+
]
|
|
7497
7917
|
};
|
|
7498
7918
|
DATE_FIELDS = {
|
|
7499
7919
|
project: ["updatedAt"],
|
|
@@ -7502,8 +7922,11 @@ var init_sync = __esm({
|
|
|
7502
7922
|
sessionResult: ["createdAt", "updatedAt"],
|
|
7503
7923
|
ticket: ["createdAt", "updatedAt"],
|
|
7504
7924
|
ticketAttachment: ["createdAt", "updatedAt"],
|
|
7505
|
-
customAgent: ["createdAt", "updatedAt"]
|
|
7925
|
+
customAgent: ["createdAt", "updatedAt"],
|
|
7926
|
+
githubIssue: ["issueCreatedAt", "issueUpdatedAt", "pulledAt", "createdAt", "updatedAt"]
|
|
7506
7927
|
};
|
|
7928
|
+
__FIELDS = FIELDS;
|
|
7929
|
+
__DATE_FIELDS = DATE_FIELDS;
|
|
7507
7930
|
__FIELDS_FOR_TEST = FIELDS;
|
|
7508
7931
|
}
|
|
7509
7932
|
});
|
|
@@ -9670,7 +10093,7 @@ var require_extension = __commonJS({
|
|
|
9670
10093
|
if (dest[name2] === void 0) dest[name2] = [elem];
|
|
9671
10094
|
else dest[name2].push(elem);
|
|
9672
10095
|
}
|
|
9673
|
-
function
|
|
10096
|
+
function parse3(header2) {
|
|
9674
10097
|
const offers = /* @__PURE__ */ Object.create(null);
|
|
9675
10098
|
let params = /* @__PURE__ */ Object.create(null);
|
|
9676
10099
|
let mustUnescape = false;
|
|
@@ -9682,8 +10105,8 @@ var require_extension = __commonJS({
|
|
|
9682
10105
|
let code = -1;
|
|
9683
10106
|
let end2 = -1;
|
|
9684
10107
|
let i = 0;
|
|
9685
|
-
for (; i <
|
|
9686
|
-
code =
|
|
10108
|
+
for (; i < header2.length; i++) {
|
|
10109
|
+
code = header2.charCodeAt(i);
|
|
9687
10110
|
if (extensionName === void 0) {
|
|
9688
10111
|
if (end2 === -1 && tokenChars[code] === 1) {
|
|
9689
10112
|
if (start === -1) start = i;
|
|
@@ -9694,7 +10117,7 @@ var require_extension = __commonJS({
|
|
|
9694
10117
|
throw new SyntaxError(`Unexpected character at index ${i}`);
|
|
9695
10118
|
}
|
|
9696
10119
|
if (end2 === -1) end2 = i;
|
|
9697
|
-
const name2 =
|
|
10120
|
+
const name2 = header2.slice(start, end2);
|
|
9698
10121
|
if (code === 44) {
|
|
9699
10122
|
push(offers, name2, params);
|
|
9700
10123
|
params = /* @__PURE__ */ Object.create(null);
|
|
@@ -9715,7 +10138,7 @@ var require_extension = __commonJS({
|
|
|
9715
10138
|
throw new SyntaxError(`Unexpected character at index ${i}`);
|
|
9716
10139
|
}
|
|
9717
10140
|
if (end2 === -1) end2 = i;
|
|
9718
|
-
push(params,
|
|
10141
|
+
push(params, header2.slice(start, end2), true);
|
|
9719
10142
|
if (code === 44) {
|
|
9720
10143
|
push(offers, extensionName, params);
|
|
9721
10144
|
params = /* @__PURE__ */ Object.create(null);
|
|
@@ -9723,7 +10146,7 @@ var require_extension = __commonJS({
|
|
|
9723
10146
|
}
|
|
9724
10147
|
start = end2 = -1;
|
|
9725
10148
|
} else if (code === 61 && start !== -1 && end2 === -1) {
|
|
9726
|
-
paramName =
|
|
10149
|
+
paramName = header2.slice(start, i);
|
|
9727
10150
|
start = end2 = -1;
|
|
9728
10151
|
} else {
|
|
9729
10152
|
throw new SyntaxError(`Unexpected character at index ${i}`);
|
|
@@ -9747,7 +10170,7 @@ var require_extension = __commonJS({
|
|
|
9747
10170
|
} else {
|
|
9748
10171
|
throw new SyntaxError(`Unexpected character at index ${i}`);
|
|
9749
10172
|
}
|
|
9750
|
-
} else if (code === 34 &&
|
|
10173
|
+
} else if (code === 34 && header2.charCodeAt(i - 1) === 61) {
|
|
9751
10174
|
inQuotes = true;
|
|
9752
10175
|
} else if (end2 === -1 && tokenChars[code] === 1) {
|
|
9753
10176
|
if (start === -1) start = i;
|
|
@@ -9758,7 +10181,7 @@ var require_extension = __commonJS({
|
|
|
9758
10181
|
throw new SyntaxError(`Unexpected character at index ${i}`);
|
|
9759
10182
|
}
|
|
9760
10183
|
if (end2 === -1) end2 = i;
|
|
9761
|
-
let value =
|
|
10184
|
+
let value = header2.slice(start, end2);
|
|
9762
10185
|
if (mustUnescape) {
|
|
9763
10186
|
value = value.replace(/\\/g, "");
|
|
9764
10187
|
mustUnescape = false;
|
|
@@ -9780,7 +10203,7 @@ var require_extension = __commonJS({
|
|
|
9780
10203
|
throw new SyntaxError("Unexpected end of input");
|
|
9781
10204
|
}
|
|
9782
10205
|
if (end2 === -1) end2 = i;
|
|
9783
|
-
const token =
|
|
10206
|
+
const token = header2.slice(start, end2);
|
|
9784
10207
|
if (extensionName === void 0) {
|
|
9785
10208
|
push(offers, token, params);
|
|
9786
10209
|
} else {
|
|
@@ -9810,7 +10233,7 @@ var require_extension = __commonJS({
|
|
|
9810
10233
|
}).join(", ");
|
|
9811
10234
|
}).join(", ");
|
|
9812
10235
|
}
|
|
9813
|
-
module.exports = { format, parse:
|
|
10236
|
+
module.exports = { format, parse: parse3 };
|
|
9814
10237
|
}
|
|
9815
10238
|
});
|
|
9816
10239
|
|
|
@@ -9823,7 +10246,7 @@ var require_websocket = __commonJS({
|
|
|
9823
10246
|
var http = __require("http");
|
|
9824
10247
|
var net = __require("net");
|
|
9825
10248
|
var tls = __require("tls");
|
|
9826
|
-
var { randomBytes:
|
|
10249
|
+
var { randomBytes: randomBytes6, createHash: createHash8 } = __require("crypto");
|
|
9827
10250
|
var { Duplex, Readable } = __require("stream");
|
|
9828
10251
|
var { URL: URL2 } = __require("url");
|
|
9829
10252
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -9844,7 +10267,7 @@ var require_websocket = __commonJS({
|
|
|
9844
10267
|
var {
|
|
9845
10268
|
EventTarget: { addEventListener, removeEventListener }
|
|
9846
10269
|
} = require_event_target();
|
|
9847
|
-
var { format, parse:
|
|
10270
|
+
var { format, parse: parse3 } = require_extension();
|
|
9848
10271
|
var { toBuffer } = require_buffer_util();
|
|
9849
10272
|
var kAborted = Symbol("kAborted");
|
|
9850
10273
|
var protocolVersions = [8, 13];
|
|
@@ -10361,7 +10784,7 @@ var require_websocket = __commonJS({
|
|
|
10361
10784
|
}
|
|
10362
10785
|
}
|
|
10363
10786
|
const defaultPort = isSecure ? 443 : 80;
|
|
10364
|
-
const key =
|
|
10787
|
+
const key = randomBytes6(16).toString("base64");
|
|
10365
10788
|
const request = isSecure ? https.request : http.request;
|
|
10366
10789
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
10367
10790
|
let perMessageDeflate;
|
|
@@ -10491,7 +10914,7 @@ var require_websocket = __commonJS({
|
|
|
10491
10914
|
abortHandshake(websocket2, socket2, "Invalid Upgrade header");
|
|
10492
10915
|
return;
|
|
10493
10916
|
}
|
|
10494
|
-
const digest =
|
|
10917
|
+
const digest = createHash8("sha1").update(key + GUID).digest("base64");
|
|
10495
10918
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
10496
10919
|
abortHandshake(websocket2, socket2, "Invalid Sec-WebSocket-Accept header");
|
|
10497
10920
|
return;
|
|
@@ -10521,7 +10944,7 @@ var require_websocket = __commonJS({
|
|
|
10521
10944
|
}
|
|
10522
10945
|
let extensions;
|
|
10523
10946
|
try {
|
|
10524
|
-
extensions =
|
|
10947
|
+
extensions = parse3(secWebSocketExtensions);
|
|
10525
10948
|
} catch (err) {
|
|
10526
10949
|
const message = "Invalid Sec-WebSocket-Extensions header";
|
|
10527
10950
|
abortHandshake(websocket2, socket2, message);
|
|
@@ -10813,13 +11236,13 @@ var require_subprotocol = __commonJS({
|
|
|
10813
11236
|
"../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/subprotocol.js"(exports, module) {
|
|
10814
11237
|
"use strict";
|
|
10815
11238
|
var { tokenChars } = require_validation();
|
|
10816
|
-
function
|
|
11239
|
+
function parse3(header2) {
|
|
10817
11240
|
const protocols = /* @__PURE__ */ new Set();
|
|
10818
11241
|
let start = -1;
|
|
10819
11242
|
let end2 = -1;
|
|
10820
11243
|
let i = 0;
|
|
10821
|
-
for (i; i <
|
|
10822
|
-
const code =
|
|
11244
|
+
for (i; i < header2.length; i++) {
|
|
11245
|
+
const code = header2.charCodeAt(i);
|
|
10823
11246
|
if (end2 === -1 && tokenChars[code] === 1) {
|
|
10824
11247
|
if (start === -1) start = i;
|
|
10825
11248
|
} else if (i !== 0 && (code === 32 || code === 9)) {
|
|
@@ -10829,7 +11252,7 @@ var require_subprotocol = __commonJS({
|
|
|
10829
11252
|
throw new SyntaxError(`Unexpected character at index ${i}`);
|
|
10830
11253
|
}
|
|
10831
11254
|
if (end2 === -1) end2 = i;
|
|
10832
|
-
const protocol2 =
|
|
11255
|
+
const protocol2 = header2.slice(start, end2);
|
|
10833
11256
|
if (protocols.has(protocol2)) {
|
|
10834
11257
|
throw new SyntaxError(`The "${protocol2}" subprotocol is duplicated`);
|
|
10835
11258
|
}
|
|
@@ -10842,14 +11265,14 @@ var require_subprotocol = __commonJS({
|
|
|
10842
11265
|
if (start === -1 || end2 !== -1) {
|
|
10843
11266
|
throw new SyntaxError("Unexpected end of input");
|
|
10844
11267
|
}
|
|
10845
|
-
const protocol =
|
|
11268
|
+
const protocol = header2.slice(start, i);
|
|
10846
11269
|
if (protocols.has(protocol)) {
|
|
10847
11270
|
throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`);
|
|
10848
11271
|
}
|
|
10849
11272
|
protocols.add(protocol);
|
|
10850
11273
|
return protocols;
|
|
10851
11274
|
}
|
|
10852
|
-
module.exports = { parse:
|
|
11275
|
+
module.exports = { parse: parse3 };
|
|
10853
11276
|
}
|
|
10854
11277
|
});
|
|
10855
11278
|
|
|
@@ -10860,7 +11283,7 @@ var require_websocket_server = __commonJS({
|
|
|
10860
11283
|
var EventEmitter = __require("events");
|
|
10861
11284
|
var http = __require("http");
|
|
10862
11285
|
var { Duplex } = __require("stream");
|
|
10863
|
-
var { createHash:
|
|
11286
|
+
var { createHash: createHash8 } = __require("crypto");
|
|
10864
11287
|
var extension2 = require_extension();
|
|
10865
11288
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
10866
11289
|
var subprotocol2 = require_subprotocol();
|
|
@@ -11167,7 +11590,7 @@ var require_websocket_server = __commonJS({
|
|
|
11167
11590
|
);
|
|
11168
11591
|
}
|
|
11169
11592
|
if (this._state > RUNNING) return abortHandshake(socket2, 503);
|
|
11170
|
-
const digest =
|
|
11593
|
+
const digest = createHash8("sha1").update(key + GUID).digest("base64");
|
|
11171
11594
|
const headers = [
|
|
11172
11595
|
"HTTP/1.1 101 Switching Protocols",
|
|
11173
11596
|
"Upgrade: websocket",
|
|
@@ -11814,13 +12237,13 @@ var require_HeaderParser = __commonJS({
|
|
|
11814
12237
|
this._parseHeader();
|
|
11815
12238
|
}
|
|
11816
12239
|
this.ss.matches = this.ss.maxMatches;
|
|
11817
|
-
const
|
|
12240
|
+
const header2 = this.header;
|
|
11818
12241
|
this.header = {};
|
|
11819
12242
|
this.buffer = "";
|
|
11820
12243
|
this.finished = true;
|
|
11821
12244
|
this.nread = this.npairs = 0;
|
|
11822
12245
|
this.maxed = false;
|
|
11823
|
-
this.emit("header",
|
|
12246
|
+
this.emit("header", header2);
|
|
11824
12247
|
};
|
|
11825
12248
|
HeaderParser.prototype._parseHeader = function() {
|
|
11826
12249
|
if (this.npairs === this.maxHeaderPairs) {
|
|
@@ -11899,9 +12322,9 @@ var require_Dicer = __commonJS({
|
|
|
11899
12322
|
this._pause = false;
|
|
11900
12323
|
const self = this;
|
|
11901
12324
|
this._hparser = new HeaderParser(cfg);
|
|
11902
|
-
this._hparser.on("header", function(
|
|
12325
|
+
this._hparser.on("header", function(header2) {
|
|
11903
12326
|
self._inHeader = false;
|
|
11904
|
-
self._part.emit("header",
|
|
12327
|
+
self._part.emit("header", header2);
|
|
11905
12328
|
});
|
|
11906
12329
|
}
|
|
11907
12330
|
inherits(Dicer, WritableStream);
|
|
@@ -12917,7 +13340,7 @@ var require_multipart = __commonJS({
|
|
|
12917
13340
|
field.emit("end");
|
|
12918
13341
|
field.removeAllListeners("end");
|
|
12919
13342
|
}
|
|
12920
|
-
part.on("header", function(
|
|
13343
|
+
part.on("header", function(header2) {
|
|
12921
13344
|
let contype;
|
|
12922
13345
|
let fieldname;
|
|
12923
13346
|
let parsed;
|
|
@@ -12925,8 +13348,8 @@ var require_multipart = __commonJS({
|
|
|
12925
13348
|
let encoding;
|
|
12926
13349
|
let filename;
|
|
12927
13350
|
let nsize = 0;
|
|
12928
|
-
if (
|
|
12929
|
-
parsed = parseParams(
|
|
13351
|
+
if (header2["content-type"]) {
|
|
13352
|
+
parsed = parseParams(header2["content-type"][0]);
|
|
12930
13353
|
if (parsed[0]) {
|
|
12931
13354
|
contype = parsed[0].toLowerCase();
|
|
12932
13355
|
for (i = 0, len = parsed.length; i < len; ++i) {
|
|
@@ -12943,8 +13366,8 @@ var require_multipart = __commonJS({
|
|
|
12943
13366
|
if (charset === void 0) {
|
|
12944
13367
|
charset = defCharset;
|
|
12945
13368
|
}
|
|
12946
|
-
if (
|
|
12947
|
-
parsed = parseParams(
|
|
13369
|
+
if (header2["content-disposition"]) {
|
|
13370
|
+
parsed = parseParams(header2["content-disposition"][0]);
|
|
12948
13371
|
if (!RE_FIELD.test(parsed[0])) {
|
|
12949
13372
|
return skipPart(part);
|
|
12950
13373
|
}
|
|
@@ -12961,8 +13384,8 @@ var require_multipart = __commonJS({
|
|
|
12961
13384
|
} else {
|
|
12962
13385
|
return skipPart(part);
|
|
12963
13386
|
}
|
|
12964
|
-
if (
|
|
12965
|
-
encoding =
|
|
13387
|
+
if (header2["content-transfer-encoding"]) {
|
|
13388
|
+
encoding = header2["content-transfer-encoding"][0].toLowerCase();
|
|
12966
13389
|
} else {
|
|
12967
13390
|
encoding = "7bit";
|
|
12968
13391
|
}
|
|
@@ -14038,7 +14461,7 @@ var require_stream_wormhole = __commonJS({
|
|
|
14038
14461
|
"../node_modules/.pnpm/stream-wormhole@1.1.0/node_modules/stream-wormhole/index.js"(exports, module) {
|
|
14039
14462
|
"use strict";
|
|
14040
14463
|
module.exports = (stream, throwError) => {
|
|
14041
|
-
return new Promise((resolve14,
|
|
14464
|
+
return new Promise((resolve14, reject2) => {
|
|
14042
14465
|
if (typeof stream.resume !== "function") {
|
|
14043
14466
|
return resolve14();
|
|
14044
14467
|
}
|
|
@@ -14062,7 +14485,7 @@ var require_stream_wormhole = __commonJS({
|
|
|
14062
14485
|
function onError(err) {
|
|
14063
14486
|
cleanup();
|
|
14064
14487
|
if (throwError) {
|
|
14065
|
-
|
|
14488
|
+
reject2(err);
|
|
14066
14489
|
} else {
|
|
14067
14490
|
resolve14();
|
|
14068
14491
|
}
|
|
@@ -14280,7 +14703,7 @@ var require_secure_json_parse = __commonJS({
|
|
|
14280
14703
|
}
|
|
14281
14704
|
return obj;
|
|
14282
14705
|
}
|
|
14283
|
-
function
|
|
14706
|
+
function parse3(text, reviver, options2) {
|
|
14284
14707
|
const stackTraceLimit = Error.stackTraceLimit;
|
|
14285
14708
|
Error.stackTraceLimit = 0;
|
|
14286
14709
|
try {
|
|
@@ -14300,9 +14723,9 @@ var require_secure_json_parse = __commonJS({
|
|
|
14300
14723
|
Error.stackTraceLimit = stackTraceLimit;
|
|
14301
14724
|
}
|
|
14302
14725
|
}
|
|
14303
|
-
module.exports =
|
|
14304
|
-
module.exports.default =
|
|
14305
|
-
module.exports.parse =
|
|
14726
|
+
module.exports = parse3;
|
|
14727
|
+
module.exports.default = parse3;
|
|
14728
|
+
module.exports.parse = parse3;
|
|
14306
14729
|
module.exports.safeParse = safeParse;
|
|
14307
14730
|
module.exports.scan = filter;
|
|
14308
14731
|
}
|
|
@@ -14499,13 +14922,13 @@ var require_multipart2 = __commonJS({
|
|
|
14499
14922
|
}
|
|
14500
14923
|
};
|
|
14501
14924
|
const parts = () => {
|
|
14502
|
-
return new Promise((resolve14,
|
|
14925
|
+
return new Promise((resolve14, reject2) => {
|
|
14503
14926
|
handle((val) => {
|
|
14504
14927
|
if (val instanceof Error) {
|
|
14505
14928
|
if (val.message === "Unexpected end of multipart data") {
|
|
14506
14929
|
resolve14(null);
|
|
14507
14930
|
} else {
|
|
14508
|
-
|
|
14931
|
+
reject2(val);
|
|
14509
14932
|
}
|
|
14510
14933
|
} else {
|
|
14511
14934
|
resolve14(val);
|
|
@@ -15339,8 +15762,10 @@ var DEFAULT_SETTING = {
|
|
|
15339
15762
|
// SPEC-376 · ADR-0080 · uji hanya yang berubah
|
|
15340
15763
|
conflict: CONFLICT_DEFAULTS,
|
|
15341
15764
|
// SPEC-383 · ADR-0081 · default sesi konflik (opt-in, mati)
|
|
15342
|
-
lead: LEAD_DEFAULTS
|
|
15765
|
+
lead: LEAD_DEFAULTS,
|
|
15343
15766
|
// SPEC-409 · ADR-0091 · hanoman-lead (master switch mati)
|
|
15767
|
+
telegram: TELEGRAM_DEFAULTS
|
|
15768
|
+
// SPEC-476 · ADR-0096 · gateway Telegram opt-in
|
|
15344
15769
|
};
|
|
15345
15770
|
async function getSetting() {
|
|
15346
15771
|
const raw = (await prisma.setting.findUnique({ where: { id: 1 } }))?.data;
|
|
@@ -15388,18 +15813,18 @@ function ensureCodexTrust(repoDir, home) {
|
|
|
15388
15813
|
canonical = realpathSync2(repoDir);
|
|
15389
15814
|
} catch {
|
|
15390
15815
|
}
|
|
15391
|
-
const
|
|
15816
|
+
const header2 = `[projects."${canonical}"]`;
|
|
15392
15817
|
try {
|
|
15393
15818
|
let existing = "";
|
|
15394
15819
|
try {
|
|
15395
15820
|
existing = readFileSync4(path, "utf8");
|
|
15396
15821
|
} catch {
|
|
15397
15822
|
}
|
|
15398
|
-
if (existing.includes(
|
|
15823
|
+
if (existing.includes(header2)) return;
|
|
15399
15824
|
mkdirSync5(codexHome(home), { recursive: true });
|
|
15400
15825
|
const lead = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
15401
15826
|
appendFileSync(path, `${lead}
|
|
15402
|
-
${
|
|
15827
|
+
${header2}
|
|
15403
15828
|
trust_level = "trusted"
|
|
15404
15829
|
`);
|
|
15405
15830
|
} catch {
|
|
@@ -15695,6 +16120,7 @@ async function readEscalation(specId, sessions = listSessions()) {
|
|
|
15695
16120
|
// src/services/spec-deps.ts
|
|
15696
16121
|
init_db();
|
|
15697
16122
|
init_src2();
|
|
16123
|
+
init_session_id();
|
|
15698
16124
|
var REASON_LABEL = {
|
|
15699
16125
|
missing: "tak ditemukan",
|
|
15700
16126
|
unfinished: "belum selesai",
|
|
@@ -15707,7 +16133,7 @@ function dependsOnOf(spec) {
|
|
|
15707
16133
|
for (const x of v) if (typeof x === "string" && x !== "" && !out3.includes(x)) out3.push(x);
|
|
15708
16134
|
return out3;
|
|
15709
16135
|
}
|
|
15710
|
-
function blockersFor(spec, deps, isMerged) {
|
|
16136
|
+
function blockersFor(spec, deps, tipOf, isMerged) {
|
|
15711
16137
|
const ids = dependsOnOf(spec);
|
|
15712
16138
|
if (ids.length === 0) return [];
|
|
15713
16139
|
const base = spec.branchFrom ?? "HEAD";
|
|
@@ -15722,7 +16148,8 @@ function blockersFor(spec, deps, isMerged) {
|
|
|
15722
16148
|
out3.push({ id, reason: "unfinished" });
|
|
15723
16149
|
continue;
|
|
15724
16150
|
}
|
|
15725
|
-
|
|
16151
|
+
const tip = tipOf(d);
|
|
16152
|
+
if (tip && !isMerged(tip, base)) out3.push({ id, reason: "unmerged" });
|
|
15726
16153
|
}
|
|
15727
16154
|
return out3;
|
|
15728
16155
|
}
|
|
@@ -15743,6 +16170,7 @@ function reaches(edges, from, target2) {
|
|
|
15743
16170
|
}
|
|
15744
16171
|
var TTL_MS = 15e3;
|
|
15745
16172
|
var mergeCache = /* @__PURE__ */ new Map();
|
|
16173
|
+
var tipCache = /* @__PURE__ */ new Map();
|
|
15746
16174
|
function mergedInto(repoDir, sha, baseRef) {
|
|
15747
16175
|
const key = `${repoDir} ${sha} ${baseRef}`;
|
|
15748
16176
|
const hit = mergeCache.get(key);
|
|
@@ -15757,7 +16185,24 @@ function mergedInto(repoDir, sha, baseRef) {
|
|
|
15757
16185
|
mergeCache.set(key, { at: now, v });
|
|
15758
16186
|
return v;
|
|
15759
16187
|
}
|
|
16188
|
+
function workTip(repoDir, dep) {
|
|
16189
|
+
if (dep.headSha) return dep.headSha;
|
|
16190
|
+
const branch = `hanoman/${sessionIdForSpec(dep.id)}`;
|
|
16191
|
+
const key = `${repoDir} ${branch}`;
|
|
16192
|
+
const hit = tipCache.get(key);
|
|
16193
|
+
const now = Date.now();
|
|
16194
|
+
if (hit && now - hit.at < TTL_MS) return hit.v;
|
|
16195
|
+
let v = null;
|
|
16196
|
+
try {
|
|
16197
|
+
v = realGit.revParse(repoDir, branch);
|
|
16198
|
+
} catch {
|
|
16199
|
+
v = null;
|
|
16200
|
+
}
|
|
16201
|
+
tipCache.set(key, { at: now, v });
|
|
16202
|
+
return v;
|
|
16203
|
+
}
|
|
15760
16204
|
var merger = (repoDir) => (sha, base) => repoDir ? mergedInto(repoDir, sha, base) : false;
|
|
16205
|
+
var tipper = (repoDir) => (d) => repoDir ? workTip(repoDir, d) : d.headSha;
|
|
15761
16206
|
var depRows = (ids) => prisma.spec.findMany({
|
|
15762
16207
|
where: { id: { in: ids } },
|
|
15763
16208
|
select: { id: true, stage: true, headSha: true }
|
|
@@ -15766,7 +16211,7 @@ async function blockersForSpec(spec, repoDir) {
|
|
|
15766
16211
|
const ids = dependsOnOf(spec);
|
|
15767
16212
|
if (ids.length === 0) return [];
|
|
15768
16213
|
const rows = await depRows(ids);
|
|
15769
|
-
return blockersFor(spec, new Map(rows.map((r) => [r.id, r])), merger(repoDir));
|
|
16214
|
+
return blockersFor(spec, new Map(rows.map((r) => [r.id, r])), tipper(repoDir), merger(repoDir));
|
|
15770
16215
|
}
|
|
15771
16216
|
async function decorateBlocked(specs) {
|
|
15772
16217
|
const ids = [...new Set(specs.flatMap(dependsOnOf))];
|
|
@@ -15782,7 +16227,8 @@ async function decorateBlocked(specs) {
|
|
|
15782
16227
|
continue;
|
|
15783
16228
|
}
|
|
15784
16229
|
if (!repos.has(s.projectId)) repos.set(s.projectId, await resolveRepoDir(s.projectId));
|
|
15785
|
-
|
|
16230
|
+
const repo = repos.get(s.projectId);
|
|
16231
|
+
out3.push({ ...s, dependsOn: own, blockedBy: blockersFor(s, deps, tipper(repo), merger(repo)) });
|
|
15786
16232
|
}
|
|
15787
16233
|
return out3;
|
|
15788
16234
|
}
|
|
@@ -16307,10 +16753,10 @@ var _Tokenizer = class {
|
|
|
16307
16753
|
item.align.push(null);
|
|
16308
16754
|
}
|
|
16309
16755
|
}
|
|
16310
|
-
for (const
|
|
16756
|
+
for (const header2 of headers) {
|
|
16311
16757
|
item.header.push({
|
|
16312
|
-
text:
|
|
16313
|
-
tokens: this.lexer.inline(
|
|
16758
|
+
text: header2,
|
|
16759
|
+
tokens: this.lexer.inline(header2)
|
|
16314
16760
|
});
|
|
16315
16761
|
}
|
|
16316
16762
|
for (const row of rows) {
|
|
@@ -17158,10 +17604,10 @@ ${quote}</blockquote>
|
|
|
17158
17604
|
return `<p>${text}</p>
|
|
17159
17605
|
`;
|
|
17160
17606
|
}
|
|
17161
|
-
table(
|
|
17607
|
+
table(header2, body) {
|
|
17162
17608
|
if (body)
|
|
17163
17609
|
body = `<tbody>${body}</tbody>`;
|
|
17164
|
-
return "<table>\n<thead>\n" +
|
|
17610
|
+
return "<table>\n<thead>\n" + header2 + "</thead>\n" + body + "</table>\n";
|
|
17165
17611
|
}
|
|
17166
17612
|
tablerow(content) {
|
|
17167
17613
|
return `<tr>
|
|
@@ -17312,12 +17758,12 @@ var _Parser = class __Parser {
|
|
|
17312
17758
|
}
|
|
17313
17759
|
case "table": {
|
|
17314
17760
|
const tableToken = token;
|
|
17315
|
-
let
|
|
17761
|
+
let header2 = "";
|
|
17316
17762
|
let cell = "";
|
|
17317
17763
|
for (let j = 0; j < tableToken.header.length; j++) {
|
|
17318
17764
|
cell += this.renderer.tablecell(this.parseInline(tableToken.header[j].tokens), { header: true, align: tableToken.align[j] });
|
|
17319
17765
|
}
|
|
17320
|
-
|
|
17766
|
+
header2 += this.renderer.tablerow(cell);
|
|
17321
17767
|
let body = "";
|
|
17322
17768
|
for (let j = 0; j < tableToken.rows.length; j++) {
|
|
17323
17769
|
const row = tableToken.rows[j];
|
|
@@ -17327,7 +17773,7 @@ var _Parser = class __Parser {
|
|
|
17327
17773
|
}
|
|
17328
17774
|
body += this.renderer.tablerow(cell);
|
|
17329
17775
|
}
|
|
17330
|
-
out3 += this.renderer.table(
|
|
17776
|
+
out3 += this.renderer.table(header2, body);
|
|
17331
17777
|
continue;
|
|
17332
17778
|
}
|
|
17333
17779
|
case "blockquote": {
|
|
@@ -18276,6 +18722,24 @@ async function notificationsFeed() {
|
|
|
18276
18722
|
return { items, unread };
|
|
18277
18723
|
}
|
|
18278
18724
|
|
|
18725
|
+
// src/services/spec-head.ts
|
|
18726
|
+
init_db();
|
|
18727
|
+
init_src2();
|
|
18728
|
+
var readHead = (wt) => {
|
|
18729
|
+
try {
|
|
18730
|
+
return realGit.headSha(wt);
|
|
18731
|
+
} catch {
|
|
18732
|
+
return null;
|
|
18733
|
+
}
|
|
18734
|
+
};
|
|
18735
|
+
async function recordHeadSha(specId, worktree, read = readHead) {
|
|
18736
|
+
const sha = read(worktree);
|
|
18737
|
+
if (!sha) return null;
|
|
18738
|
+
await prisma.spec.update({ where: { id: specId }, data: { headSha: sha } }).catch(() => {
|
|
18739
|
+
});
|
|
18740
|
+
return sha;
|
|
18741
|
+
}
|
|
18742
|
+
|
|
18279
18743
|
// src/services/live-specs.ts
|
|
18280
18744
|
async function liveSpecs(filter = {}) {
|
|
18281
18745
|
const specs = await prisma.spec.findMany({
|
|
@@ -18291,14 +18755,16 @@ async function liveSpecs(filter = {}) {
|
|
|
18291
18755
|
if (!entry) return s;
|
|
18292
18756
|
const next = stageForRun(entry.phases, entry.cwd, s.id);
|
|
18293
18757
|
if (!next || STAGES.indexOf(next) <= STAGES.indexOf(s.stage)) return s;
|
|
18294
|
-
advanced.push({ id: s.id, from: s.stage, stage: next });
|
|
18758
|
+
advanced.push({ id: s.id, from: s.stage, stage: next, cwd: entry.cwd });
|
|
18295
18759
|
if (next === "done") doneNow.push({ specId: s.id, title: s.title, projectId: s.projectId });
|
|
18296
18760
|
return { ...s, stage: next };
|
|
18297
18761
|
});
|
|
18298
18762
|
if (advanced.length)
|
|
18299
18763
|
await Promise.all(advanced.map(async (a) => {
|
|
18300
18764
|
const res = await prisma.spec.updateMany({ where: { id: a.id, stage: a.from }, data: { stage: a.stage } }).catch(() => ({ count: 0 }));
|
|
18301
|
-
if (res.count
|
|
18765
|
+
if (res.count === 0) return;
|
|
18766
|
+
if (a.stage === "done") await recordHeadSha(a.id, a.cwd);
|
|
18767
|
+
await notifySynced("spec", a.id);
|
|
18302
18768
|
}));
|
|
18303
18769
|
await Promise.all(doneNow.map((d) => recordCompletion(d.specId, d.title, d.projectId)));
|
|
18304
18770
|
return decorateBlocked(out3);
|
|
@@ -20093,6 +20559,18 @@ async function terminal_default(app2) {
|
|
|
20093
20559
|
if (!s?.flow || !phases) return reply.code(404).send({ error: "not found" });
|
|
20094
20560
|
return { flow: s.flow, phases };
|
|
20095
20561
|
});
|
|
20562
|
+
app2.post("/terminal/sessions/:id/steer", async (req, reply) => {
|
|
20563
|
+
const parsed = zTerminalSteerInput.safeParse(req.body);
|
|
20564
|
+
if (!parsed.success) return reply.code(400).send({ error: "invalid body" });
|
|
20565
|
+
const ok2 = await sendToPane(req.params.id, parsed.data.text);
|
|
20566
|
+
if (!ok2) return reply.code(404).send({ error: "live session not found" });
|
|
20567
|
+
return reply.code(202).send({ accepted: true });
|
|
20568
|
+
});
|
|
20569
|
+
app2.post("/terminal/sessions/:id/interrupt", async (req, reply) => {
|
|
20570
|
+
const ok2 = interruptPane(req.params.id);
|
|
20571
|
+
if (!ok2) return reply.code(404).send({ error: "live session not found" });
|
|
20572
|
+
return reply.code(202).send({ accepted: true });
|
|
20573
|
+
});
|
|
20096
20574
|
const sessionWorktree = async (id) => {
|
|
20097
20575
|
const s = getSession(id);
|
|
20098
20576
|
if (!s) return { ok: false, code: 404, msg: "not found" };
|
|
@@ -20156,11 +20634,7 @@ async function terminal_default(app2) {
|
|
|
20156
20634
|
if (repoDir) {
|
|
20157
20635
|
if (s.specId) {
|
|
20158
20636
|
if (s.flow) await advanceStage(s.specId, repoDir, id, s.flow, s.cwd);
|
|
20159
|
-
|
|
20160
|
-
const headSha = realGit.headSha(s.cwd);
|
|
20161
|
-
await prisma.spec.update({ where: { id: s.specId }, data: { headSha } });
|
|
20162
|
-
} catch {
|
|
20163
|
-
}
|
|
20637
|
+
await recordHeadSha(s.specId, s.cwd);
|
|
20164
20638
|
}
|
|
20165
20639
|
killSession(id);
|
|
20166
20640
|
if (ownsWorktree(repoDir, s.cwd)) realGit.removeWorktree(repoDir, s.cwd);
|
|
@@ -21665,11 +22139,11 @@ function clamp(text) {
|
|
|
21665
22139
|
const buf = Buffer.from(text, "utf8");
|
|
21666
22140
|
if (buf.byteLength <= MAX_TRANSCRIPT_BYTES) return { body: text, truncated: false };
|
|
21667
22141
|
const cut = buf.byteLength - MAX_TRANSCRIPT_BYTES;
|
|
21668
|
-
let
|
|
21669
|
-
const nl =
|
|
21670
|
-
if (nl >= 0)
|
|
22142
|
+
let tail3 = buf.subarray(cut).toString("utf8");
|
|
22143
|
+
const nl = tail3.indexOf("\n");
|
|
22144
|
+
if (nl >= 0) tail3 = tail3.slice(nl + 1);
|
|
21671
22145
|
return { body: `\u2026 ${cut} byte awal dipangkas (batas ${MAX_TRANSCRIPT_BYTES} byte) \u2026
|
|
21672
|
-
${
|
|
22146
|
+
${tail3}`, truncated: true };
|
|
21673
22147
|
}
|
|
21674
22148
|
async function saveTranscript(text) {
|
|
21675
22149
|
if (!text.trim()) return { key: "", bytes: 0, truncated: false };
|
|
@@ -22629,10 +23103,22 @@ function leadArgv(o) {
|
|
|
22629
23103
|
];
|
|
22630
23104
|
}
|
|
22631
23105
|
var leadEnv = (agent, base = process.env, uid = process.getuid?.()) => agent === "claude" ? { ...rootBypassEnv(uid), ...base } : { ...base };
|
|
23106
|
+
var EXPLAIN_MAX = 500;
|
|
23107
|
+
var tail = (s) => {
|
|
23108
|
+
const t = s.trim();
|
|
23109
|
+
return t.length > EXPLAIN_MAX ? `\u2026${t.slice(-EXPLAIN_MAX)}` : t;
|
|
23110
|
+
};
|
|
23111
|
+
function leadFailureReason(agent, timeoutMs, err, stdout, stderr) {
|
|
23112
|
+
if (err.killed) return `lead ${agent} kehabisan waktu ${timeoutMs} ms`;
|
|
23113
|
+
const fromErr = err.message.startsWith("Command failed:") ? "" : err.message;
|
|
23114
|
+
const detail = [tail(stderr), tail(stdout)].filter(Boolean).join(" \xB7 ") || tail(fromErr) || "tanpa keluaran";
|
|
23115
|
+
const how = err.signal ? `sinyal ${err.signal}` : `exit ${err.code ?? "?"}`;
|
|
23116
|
+
return `lead ${agent} gagal (${how}): ${detail}`;
|
|
23117
|
+
}
|
|
22632
23118
|
function think(prompt, o) {
|
|
22633
23119
|
const bin = binFor(o.agent);
|
|
22634
23120
|
const args = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
|
|
22635
|
-
return new Promise((resolve14,
|
|
23121
|
+
return new Promise((resolve14, reject2) => {
|
|
22636
23122
|
const child = execFile9(bin, args, {
|
|
22637
23123
|
cwd: o.cwd,
|
|
22638
23124
|
timeout: o.timeoutMs,
|
|
@@ -22642,8 +23128,7 @@ function think(prompt, o) {
|
|
|
22642
23128
|
killSignal: "SIGTERM"
|
|
22643
23129
|
}, (err, stdout, stderr) => {
|
|
22644
23130
|
if (err) {
|
|
22645
|
-
|
|
22646
|
-
reject(new Error(killed ? `lead ${o.agent} kehabisan waktu ${o.timeoutMs} ms` : `lead ${o.agent} gagal: ${(stderr || err.message).trim().slice(0, 500)}`));
|
|
23131
|
+
reject2(new Error(leadFailureReason(o.agent, o.timeoutMs, err, stdout, stderr)));
|
|
22647
23132
|
return;
|
|
22648
23133
|
}
|
|
22649
23134
|
resolve14(stdout);
|
|
@@ -22921,12 +23406,13 @@ Bukti integrasi: ${evidence.join("; ")} \u2192 ${verdict}.` }
|
|
|
22921
23406
|
|
|
22922
23407
|
// src/services/lead/detect.ts
|
|
22923
23408
|
init_pty();
|
|
23409
|
+
init_tui_dialog();
|
|
22924
23410
|
import { writeFileSync as writeFileSync4 } from "node:fs";
|
|
22925
23411
|
|
|
22926
23412
|
// src/services/lead/pane.ts
|
|
22927
23413
|
init_tui_dialog();
|
|
22928
23414
|
var CLEAN = /[│┃┆┊┌┐└┘├┤┬┴┼─━╭╮╰╯>❯]/g;
|
|
22929
|
-
var
|
|
23415
|
+
var tail2 = (text, lines2) => text.split("\n").map((l) => l.replace(CLEAN, " ").trimEnd()).filter((l) => l.trim()).slice(-lines2);
|
|
22930
23416
|
var CODEX_FINISHED = [
|
|
22931
23417
|
/Goal achieved/i,
|
|
22932
23418
|
/Goal unmet/i,
|
|
@@ -22939,11 +23425,11 @@ var ASK_SIGNALS = [
|
|
|
22939
23425
|
/\b(pilih|apakah|haruskah|mana yang|opsi|which|should i|do you want|proceed\?)\b/i
|
|
22940
23426
|
];
|
|
22941
23427
|
function readPaneQuestion(text, agent) {
|
|
22942
|
-
const lines2 =
|
|
23428
|
+
const lines2 = tail2(text, 40);
|
|
22943
23429
|
const body = lines2.join("\n").trim();
|
|
22944
23430
|
const choices = readChoiceDialog(text)?.options ?? [];
|
|
22945
23431
|
if (!body) return { asking: false, question: "", reason: "layar kosong", choices };
|
|
22946
|
-
const question =
|
|
23432
|
+
const question = tail2(text, 25).join("\n").trim();
|
|
22947
23433
|
if (agent === "codex") {
|
|
22948
23434
|
const finished = CODEX_FINISHED.find((re) => re.test(body));
|
|
22949
23435
|
if (finished) return { asking: false, question, reason: "sesi codex selesai wajar (ADR-0074)", choices };
|
|
@@ -22956,9 +23442,16 @@ function readPaneQuestion(text, agent) {
|
|
|
22956
23442
|
// src/services/lead/detect.ts
|
|
22957
23443
|
var answers = /* @__PURE__ */ new Map();
|
|
22958
23444
|
var capped = /* @__PURE__ */ new Set();
|
|
23445
|
+
var failures = /* @__PURE__ */ new Map();
|
|
23446
|
+
var failCapped = /* @__PURE__ */ new Set();
|
|
23447
|
+
var MAX_CHAIN_STEPS = 6;
|
|
23448
|
+
var CHAIN_POLL_MS = 300;
|
|
23449
|
+
var CHAIN_POLL_TRIES = 20;
|
|
22959
23450
|
function resetSession(sessionId2) {
|
|
22960
23451
|
answers.delete(sessionId2);
|
|
22961
23452
|
capped.delete(sessionId2);
|
|
23453
|
+
failures.delete(sessionId2);
|
|
23454
|
+
failCapped.delete(sessionId2);
|
|
22962
23455
|
}
|
|
22963
23456
|
var prodDetectDeps = {
|
|
22964
23457
|
live: () => {
|
|
@@ -22994,6 +23487,8 @@ var prodDetectDeps = {
|
|
|
22994
23487
|
} catch {
|
|
22995
23488
|
}
|
|
22996
23489
|
},
|
|
23490
|
+
submit: (id) => submitPaneDialog(id),
|
|
23491
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
22997
23492
|
decide,
|
|
22998
23493
|
decideDeps: prodDecideDeps,
|
|
22999
23494
|
optIn: leadProjects,
|
|
@@ -23025,7 +23520,7 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
23025
23520
|
if ((answers.get(s.id) ?? 0) >= cfg.maxAutoAnswers) {
|
|
23026
23521
|
if (!capped.has(s.id)) {
|
|
23027
23522
|
capped.add(s.id);
|
|
23028
|
-
const
|
|
23523
|
+
const row = await recordDecision({
|
|
23029
23524
|
projectId: s.projectId,
|
|
23030
23525
|
specId: s.specId,
|
|
23031
23526
|
sessionId: s.id,
|
|
@@ -23040,7 +23535,7 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
23040
23535
|
weighty: true
|
|
23041
23536
|
});
|
|
23042
23537
|
await deps.notify(
|
|
23043
|
-
|
|
23538
|
+
row.id,
|
|
23044
23539
|
`Lead berhenti menjawab sesi ${s.id} (batas ${cfg.maxAutoAnswers}\xD7 tercapai)`,
|
|
23045
23540
|
s.projectId,
|
|
23046
23541
|
s.specId ?? null,
|
|
@@ -23050,16 +23545,79 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
23050
23545
|
skip("batas jawaban otomatis tercapai");
|
|
23051
23546
|
continue;
|
|
23052
23547
|
}
|
|
23548
|
+
if ((failures.get(s.id) ?? 0) >= cfg.maxAutoAnswers) {
|
|
23549
|
+
if (!failCapped.has(s.id)) {
|
|
23550
|
+
failCapped.add(s.id);
|
|
23551
|
+
const row = await recordDecision({
|
|
23552
|
+
projectId: s.projectId,
|
|
23553
|
+
specId: s.specId,
|
|
23554
|
+
sessionId: s.id,
|
|
23555
|
+
gate: "detected",
|
|
23556
|
+
kind: "quality",
|
|
23557
|
+
question: `Keputusan untuk sesi ${s.id} gagal disusun ${cfg.maxAutoAnswers}\xD7 berturut-turut.`,
|
|
23558
|
+
answer: "Berhenti mencoba; serahkan ke operator.",
|
|
23559
|
+
reason: "Kegagalan beruntun menandakan sebab yang tak hilang dengan mengulang (kunci/kuota agen, biner tak terpasang) \u2014 mencoba lagi tiap denyut hanya membakar kuota. Alasan percobaan terakhir ada di baris jejak `gagal` tepat di atas ini.",
|
|
23560
|
+
refs: [],
|
|
23561
|
+
confidence: "tinggi",
|
|
23562
|
+
action: "none",
|
|
23563
|
+
weighty: true
|
|
23564
|
+
});
|
|
23565
|
+
await deps.notify(
|
|
23566
|
+
row.id,
|
|
23567
|
+
`Lead berhenti mencoba sesi ${s.id} (${cfg.maxAutoAnswers}\xD7 gagal berturut-turut)`,
|
|
23568
|
+
s.projectId,
|
|
23569
|
+
s.specId ?? null,
|
|
23570
|
+
s.id
|
|
23571
|
+
);
|
|
23572
|
+
}
|
|
23573
|
+
skip("batas kegagalan beruntun tercapai");
|
|
23574
|
+
continue;
|
|
23575
|
+
}
|
|
23053
23576
|
const agent = deps.agentOf(s.id) ?? "claude";
|
|
23054
23577
|
const read = readPaneQuestion(deps.pane(s.id), agent);
|
|
23055
23578
|
if (!read.asking) {
|
|
23056
23579
|
skip(read.reason);
|
|
23057
23580
|
continue;
|
|
23058
23581
|
}
|
|
23582
|
+
const chain = await runChain(s, agent, deps);
|
|
23583
|
+
if (chain.acted && chain.done) {
|
|
23584
|
+
deps.clearMarker(s.decisionFile);
|
|
23585
|
+
answers.set(s.id, (answers.get(s.id) ?? 0) + 1);
|
|
23586
|
+
failures.delete(s.id);
|
|
23587
|
+
out3.answered.push(s.id);
|
|
23588
|
+
continue;
|
|
23589
|
+
}
|
|
23590
|
+
if (chain.failed) failures.set(s.id, (failures.get(s.id) ?? 0) + 1);
|
|
23591
|
+
skip(chain.reason);
|
|
23592
|
+
}
|
|
23593
|
+
return out3;
|
|
23594
|
+
}
|
|
23595
|
+
async function runChain(s, agent, deps) {
|
|
23596
|
+
let acted = false;
|
|
23597
|
+
for (let step = 0; step < MAX_CHAIN_STEPS; step++) {
|
|
23598
|
+
const text = deps.pane(s.id);
|
|
23599
|
+
const screen = readDialogScreen(text);
|
|
23600
|
+
if (screen?.kind === "review") {
|
|
23601
|
+
if (!await deps.submit(s.id))
|
|
23602
|
+
return { acted, done: false, failed: true, reason: "gagal menekan Submit answers" };
|
|
23603
|
+
acted = true;
|
|
23604
|
+
continue;
|
|
23605
|
+
}
|
|
23606
|
+
if (step > 0 && !screen) return { acted, done: true, failed: false, reason: "" };
|
|
23607
|
+
const read = readPaneQuestion(text, agent);
|
|
23608
|
+
if (!read.asking) {
|
|
23609
|
+
return step === 0 ? { acted, done: false, failed: false, reason: read.reason } : { acted, done: true, failed: false, reason: "" };
|
|
23610
|
+
}
|
|
23059
23611
|
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\`).`];
|
|
23060
23612
|
if (read.choices.length) {
|
|
23061
23613
|
notes.push("Layarnya adalah dialog pilihan. `reply` akan dimasukkan sebagai JAWABAN BEBAS ke dialog itu, jadi tulislah jawaban yang berdiri sendiri \u2014 sebut opsi yang kamu pilih beserta alasan singkatnya, bukan nomornya saja.");
|
|
23062
23614
|
}
|
|
23615
|
+
if (screen?.kind === "question" && screen.tabs.length > 1) {
|
|
23616
|
+
const at = screen.tabs.findIndex((t) => !t.answered);
|
|
23617
|
+
notes.push(
|
|
23618
|
+
`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.`
|
|
23619
|
+
);
|
|
23620
|
+
}
|
|
23063
23621
|
const row = await deps.decide({
|
|
23064
23622
|
projectId: s.projectId,
|
|
23065
23623
|
specId: s.specId,
|
|
@@ -23070,26 +23628,32 @@ async function scanAndAnswer(deps = prodDetectDeps) {
|
|
|
23070
23628
|
options: read.choices.length ? read.choices : void 0,
|
|
23071
23629
|
notes
|
|
23072
23630
|
}, deps.decideDeps);
|
|
23073
|
-
if (!row
|
|
23074
|
-
|
|
23075
|
-
|
|
23076
|
-
}
|
|
23631
|
+
if (!row) return { acted, done: false, failed: false, reason: "lead tak menghasilkan keputusan yang berlaku" };
|
|
23632
|
+
if (row.status !== "berlaku")
|
|
23633
|
+
return { acted, done: false, failed: true, reason: "lead tak menghasilkan keputusan yang berlaku" };
|
|
23077
23634
|
const reply = takeReply(row.id) || row.answer;
|
|
23078
|
-
|
|
23079
|
-
|
|
23080
|
-
|
|
23081
|
-
|
|
23082
|
-
|
|
23083
|
-
|
|
23084
|
-
answers.set(s.id, (answers.get(s.id) ?? 0) + 1);
|
|
23085
|
-
out3.answered.push(s.id);
|
|
23635
|
+
if (!await deps.send(s.id, reply))
|
|
23636
|
+
return { acted, done: false, failed: true, reason: "gagal mengetik ke pane" };
|
|
23637
|
+
acted = true;
|
|
23638
|
+
if (!screen) return { acted, done: true, failed: false, reason: "" };
|
|
23639
|
+
if (!await waitScreenChange(s.id, dialogKey(text), deps))
|
|
23640
|
+
return { acted, done: false, failed: true, reason: "layar dialog tak berubah sesudah dijawab" };
|
|
23086
23641
|
}
|
|
23087
|
-
return
|
|
23642
|
+
return { acted, done: false, failed: true, reason: "batas langkah rantai dialog tercapai" };
|
|
23643
|
+
}
|
|
23644
|
+
async function waitScreenChange(id, before, deps) {
|
|
23645
|
+
for (let i = 0; i < CHAIN_POLL_TRIES; i++) {
|
|
23646
|
+
await deps.sleep(CHAIN_POLL_MS);
|
|
23647
|
+
if (dialogKey(deps.pane(id)) !== before) return true;
|
|
23648
|
+
}
|
|
23649
|
+
return false;
|
|
23088
23650
|
}
|
|
23089
23651
|
function sweep(liveIds) {
|
|
23090
23652
|
const live = new Set(liveIds);
|
|
23091
23653
|
for (const id of [...answers.keys()]) if (!live.has(id)) answers.delete(id);
|
|
23092
23654
|
for (const id of [...capped]) if (!live.has(id)) capped.delete(id);
|
|
23655
|
+
for (const id of [...failures.keys()]) if (!live.has(id)) failures.delete(id);
|
|
23656
|
+
for (const id of [...failCapped]) if (!live.has(id)) failCapped.delete(id);
|
|
23093
23657
|
}
|
|
23094
23658
|
|
|
23095
23659
|
// src/services/lead/pulse.ts
|
|
@@ -23715,60 +24279,832 @@ async function custom_agents_default(app2) {
|
|
|
23715
24279
|
});
|
|
23716
24280
|
}
|
|
23717
24281
|
|
|
23718
|
-
// src/
|
|
23719
|
-
|
|
24282
|
+
// src/routes/github-issues.ts
|
|
24283
|
+
init_zod();
|
|
24284
|
+
init_db();
|
|
23720
24285
|
|
|
23721
|
-
// src/
|
|
23722
|
-
init_src();
|
|
24286
|
+
// src/services/github-issues.ts
|
|
23723
24287
|
init_db();
|
|
23724
24288
|
|
|
23725
|
-
// src/services/
|
|
24289
|
+
// src/services/github-repo.ts
|
|
23726
24290
|
init_db();
|
|
23727
|
-
|
|
23728
|
-
|
|
23729
|
-
|
|
23730
|
-
|
|
23731
|
-
|
|
23732
|
-
|
|
23733
|
-
|
|
23734
|
-
|
|
23735
|
-
|
|
23736
|
-
|
|
23737
|
-
|
|
23738
|
-
|
|
23739
|
-
|
|
23740
|
-
const
|
|
23741
|
-
|
|
23742
|
-
|
|
24291
|
+
function parse2(url2) {
|
|
24292
|
+
const u = url2.trim();
|
|
24293
|
+
let m = /^git@([^:]+):([^/]+)\/(.+?)(?:\.git)?$/.exec(u);
|
|
24294
|
+
if (!m) m = /^https?:\/\/(?:[^@/]+@)?([^/]+)\/([^/]+)\/(.+?)(?:\.git)?\/?$/.exec(u);
|
|
24295
|
+
if (!m || !m[1] || !m[2] || !m[3]) return null;
|
|
24296
|
+
return { host: m[1], owner: m[2], repo: m[3] };
|
|
24297
|
+
}
|
|
24298
|
+
function githubSlugFromUrl(url2) {
|
|
24299
|
+
const p = parse2(url2);
|
|
24300
|
+
if (!p || !p.host.includes("github.")) return null;
|
|
24301
|
+
return { owner: p.owner, repo: p.repo, slug: `${p.owner}/${p.repo}` };
|
|
24302
|
+
}
|
|
24303
|
+
async function resolveGithubRepo(projectId) {
|
|
24304
|
+
const project = await prisma.project.findUnique({ where: { id: projectId } });
|
|
24305
|
+
if (!project) return { ok: false, kind: "no-project", error: `project "${projectId}" tidak ada` };
|
|
24306
|
+
const candidates = [];
|
|
24307
|
+
if (project.gitRemote) candidates.push(project.gitRemote);
|
|
24308
|
+
const repoDir = await resolveRepoDir(projectId).catch(() => null);
|
|
24309
|
+
if (repoDir) {
|
|
24310
|
+
const origin = (await listRemotes(repoDir)).find((r) => r.name === "origin");
|
|
24311
|
+
if (origin?.fetch) candidates.push(origin.fetch);
|
|
24312
|
+
}
|
|
24313
|
+
if (candidates.length === 0)
|
|
24314
|
+
return {
|
|
24315
|
+
ok: false,
|
|
24316
|
+
kind: "no-remote",
|
|
24317
|
+
error: "project belum punya remote GitHub (isi gitRemote atau tambahkan origin di repo lokalnya)"
|
|
24318
|
+
};
|
|
24319
|
+
for (const url2 of candidates) {
|
|
24320
|
+
const gh = githubSlugFromUrl(url2);
|
|
24321
|
+
if (gh) return { ok: true, repo: gh };
|
|
24322
|
+
}
|
|
24323
|
+
const host2 = parse2(candidates[0])?.host ?? candidates[0];
|
|
24324
|
+
return {
|
|
24325
|
+
ok: false,
|
|
24326
|
+
kind: "not-github",
|
|
24327
|
+
error: `remote project ber-host "${host2}", bukan GitHub \u2014 tarik issue hanya mendukung GitHub`
|
|
24328
|
+
};
|
|
23743
24329
|
}
|
|
23744
|
-
|
|
23745
|
-
|
|
23746
|
-
|
|
23747
|
-
|
|
23748
|
-
|
|
23749
|
-
|
|
23750
|
-
|
|
23751
|
-
return
|
|
24330
|
+
|
|
24331
|
+
// src/services/github-fetch.ts
|
|
24332
|
+
init_config2();
|
|
24333
|
+
import { execFile as execFile10 } from "node:child_process";
|
|
24334
|
+
var labelNames = (l) => (l ?? []).map((x) => typeof x === "string" ? x : x.name ?? "").filter(Boolean);
|
|
24335
|
+
var norm = (s) => s.toLowerCase() === "closed" ? "closed" : "open";
|
|
24336
|
+
function issueFromGh(raw) {
|
|
24337
|
+
return {
|
|
24338
|
+
number: raw.number,
|
|
24339
|
+
title: raw.title,
|
|
24340
|
+
body: raw.body ?? "",
|
|
24341
|
+
authorLogin: raw.author?.login ?? "",
|
|
24342
|
+
labels: labelNames(raw.labels),
|
|
24343
|
+
url: raw.url,
|
|
24344
|
+
issueState: norm(raw.state),
|
|
24345
|
+
issueCreatedAt: raw.createdAt,
|
|
24346
|
+
issueUpdatedAt: raw.updatedAt
|
|
24347
|
+
};
|
|
23752
24348
|
}
|
|
23753
|
-
|
|
23754
|
-
|
|
23755
|
-
|
|
23756
|
-
|
|
23757
|
-
|
|
24349
|
+
function issuesFromRest(raw) {
|
|
24350
|
+
let skippedPullRequests = 0;
|
|
24351
|
+
const issues = [];
|
|
24352
|
+
for (const r of raw) {
|
|
24353
|
+
if (r.pull_request !== void 0) {
|
|
24354
|
+
skippedPullRequests++;
|
|
24355
|
+
continue;
|
|
24356
|
+
}
|
|
24357
|
+
issues.push({
|
|
24358
|
+
number: r.number,
|
|
24359
|
+
title: r.title,
|
|
24360
|
+
body: r.body ?? "",
|
|
24361
|
+
authorLogin: r.user?.login ?? "",
|
|
24362
|
+
labels: labelNames(r.labels),
|
|
24363
|
+
url: r.html_url,
|
|
24364
|
+
issueState: norm(r.state),
|
|
24365
|
+
issueCreatedAt: r.created_at,
|
|
24366
|
+
issueUpdatedAt: r.updated_at
|
|
23758
24367
|
});
|
|
23759
|
-
return null;
|
|
23760
24368
|
}
|
|
23761
|
-
return {
|
|
24369
|
+
return { issues, skippedPullRequests };
|
|
23762
24370
|
}
|
|
23763
|
-
|
|
23764
|
-
|
|
23765
|
-
|
|
23766
|
-
|
|
23767
|
-
|
|
23768
|
-
|
|
24371
|
+
var GH_FIELDS = "number,title,body,author,labels,url,state,createdAt,updatedAt";
|
|
24372
|
+
var API2 = "https://api.github.com";
|
|
24373
|
+
var defaultRunGh = (args, env) => new Promise((resolve14, reject2) => {
|
|
24374
|
+
execFile10(
|
|
24375
|
+
args[0],
|
|
24376
|
+
args.slice(1),
|
|
24377
|
+
{ env, maxBuffer: 1 << 26, encoding: "utf8", timeout: 6e4 },
|
|
24378
|
+
(err, stdout, stderr) => {
|
|
24379
|
+
const e = err;
|
|
24380
|
+
if (e && (e.code === "ENOENT" || e.code === "EACCES")) return reject2(e);
|
|
24381
|
+
resolve14({ code: err ? Number(err.code ?? 1) : 0, stdout, stderr });
|
|
24382
|
+
}
|
|
24383
|
+
);
|
|
24384
|
+
});
|
|
24385
|
+
var defaultHttpGet = async (url2, headers) => {
|
|
24386
|
+
const res = await fetch(url2, {
|
|
24387
|
+
headers: { Accept: "application/vnd.github+json", "User-Agent": "hanoman", ...headers }
|
|
23769
24388
|
});
|
|
24389
|
+
const json = await res.json().catch(() => null);
|
|
24390
|
+
return { status: res.status, json };
|
|
24391
|
+
};
|
|
24392
|
+
function classifyGhStderr(stderr) {
|
|
24393
|
+
const s = stderr.toLowerCase();
|
|
24394
|
+
if (s.includes("gh auth login") || s.includes("bad credentials") || s.includes("http 401")) return "unauth";
|
|
24395
|
+
if (s.includes("disabled issues")) return "issues-disabled";
|
|
24396
|
+
if (s.includes("could not resolve to a repository") || s.includes("http 404")) return "not-found";
|
|
24397
|
+
return "other";
|
|
23770
24398
|
}
|
|
23771
|
-
|
|
24399
|
+
async function viaGh(repo, opts, deps) {
|
|
24400
|
+
const bin = deps.ghBin ?? effectiveStr("HANOMAN_GH_BIN") ?? "gh";
|
|
24401
|
+
const args = [
|
|
24402
|
+
bin,
|
|
24403
|
+
"issue",
|
|
24404
|
+
"list",
|
|
24405
|
+
"--repo",
|
|
24406
|
+
repo.slug,
|
|
24407
|
+
"--state",
|
|
24408
|
+
opts.state,
|
|
24409
|
+
"--limit",
|
|
24410
|
+
String(opts.limit),
|
|
24411
|
+
"--json",
|
|
24412
|
+
GH_FIELDS
|
|
24413
|
+
];
|
|
24414
|
+
const env = { ...process.env };
|
|
24415
|
+
if (deps.token) env.GH_TOKEN = deps.token;
|
|
24416
|
+
let out3;
|
|
24417
|
+
try {
|
|
24418
|
+
out3 = await (deps.runGh ?? defaultRunGh)(args, env);
|
|
24419
|
+
} catch {
|
|
24420
|
+
return { fallback: true, reason: "gh tak terpasang" };
|
|
24421
|
+
}
|
|
24422
|
+
if (out3.code !== 0) {
|
|
24423
|
+
const kind = classifyGhStderr(out3.stderr);
|
|
24424
|
+
if (kind === "unauth") return { fallback: true, reason: "gh tak terautentikasi" };
|
|
24425
|
+
return { ok: false, kind, error: out3.stderr.trim() || `gh keluar dengan kode ${out3.code}` };
|
|
24426
|
+
}
|
|
24427
|
+
let raw;
|
|
24428
|
+
try {
|
|
24429
|
+
raw = JSON.parse(out3.stdout || "[]");
|
|
24430
|
+
} catch {
|
|
24431
|
+
return { ok: false, kind: "other", error: "keluaran gh bukan JSON" };
|
|
24432
|
+
}
|
|
24433
|
+
return { ok: true, issues: raw.map(issueFromGh), via: "gh", skippedPullRequests: 0 };
|
|
24434
|
+
}
|
|
24435
|
+
async function viaRest(repo, opts, deps) {
|
|
24436
|
+
const get = deps.httpGet ?? defaultHttpGet;
|
|
24437
|
+
const headers = {};
|
|
24438
|
+
if (deps.token) headers.Authorization = `Bearer ${deps.token}`;
|
|
24439
|
+
const meta = await get(`${API2}/repos/${repo.slug}`, headers);
|
|
24440
|
+
if (meta.status === 404)
|
|
24441
|
+
return { ok: false, kind: "not-found", error: `repo "${repo.slug}" tak ditemukan atau tak terjangkau` };
|
|
24442
|
+
if (meta.status === 401 || meta.status === 403)
|
|
24443
|
+
return { ok: false, kind: "unauthorized", error: "GitHub menolak kredensial \u2014 isi GITHUB_TOKEN di Settings" };
|
|
24444
|
+
if (meta.status !== 200) return { ok: false, kind: "other", error: `GitHub menjawab HTTP ${meta.status}` };
|
|
24445
|
+
if (meta.json?.has_issues === false)
|
|
24446
|
+
return { ok: false, kind: "issues-disabled", error: `repo "${repo.slug}" mematikan fitur issue` };
|
|
24447
|
+
const issues = [];
|
|
24448
|
+
let skippedPullRequests = 0;
|
|
24449
|
+
for (let page = 1; issues.length < opts.limit && page <= 10; page++) {
|
|
24450
|
+
const per = Math.min(100, opts.limit - issues.length);
|
|
24451
|
+
const res = await get(
|
|
24452
|
+
`${API2}/repos/${repo.slug}/issues?state=${opts.state}&per_page=${per}&page=${page}`,
|
|
24453
|
+
headers
|
|
24454
|
+
);
|
|
24455
|
+
if (res.status === 404) return { ok: false, kind: "not-found", error: `repo "${repo.slug}" tak ditemukan` };
|
|
24456
|
+
if (res.status === 401 || res.status === 403)
|
|
24457
|
+
return { ok: false, kind: "unauthorized", error: "GitHub menolak kredensial" };
|
|
24458
|
+
if (res.status !== 200) return { ok: false, kind: "other", error: `GitHub menjawab HTTP ${res.status}` };
|
|
24459
|
+
const batch = Array.isArray(res.json) ? res.json : [];
|
|
24460
|
+
if (batch.length === 0) break;
|
|
24461
|
+
const n = issuesFromRest(batch);
|
|
24462
|
+
issues.push(...n.issues);
|
|
24463
|
+
skippedPullRequests += n.skippedPullRequests;
|
|
24464
|
+
if (batch.length < per) break;
|
|
24465
|
+
}
|
|
24466
|
+
return { ok: true, issues: issues.slice(0, opts.limit), via: "rest", skippedPullRequests };
|
|
24467
|
+
}
|
|
24468
|
+
async function fetchIssues(repo, opts, deps = {}) {
|
|
24469
|
+
const token = deps.token ?? effectiveStr("GITHUB_TOKEN") ?? void 0;
|
|
24470
|
+
const first = await viaGh(repo, opts, { ...deps, token });
|
|
24471
|
+
if (!("fallback" in first)) return first;
|
|
24472
|
+
return viaRest(repo, opts, { ...deps, token });
|
|
24473
|
+
}
|
|
24474
|
+
|
|
24475
|
+
// src/services/github-issues.ts
|
|
24476
|
+
var issueRowId = (projectId, slug, number) => `${projectId}:${slug}#${number}`;
|
|
24477
|
+
async function pullIssues(projectId, opts = {}, deps = {}) {
|
|
24478
|
+
const resolved = await resolveGithubRepo(projectId);
|
|
24479
|
+
if (!resolved.ok) return { ok: false, kind: resolved.kind, error: resolved.error };
|
|
24480
|
+
const state = opts.state ?? "open";
|
|
24481
|
+
const limit = Math.min(Math.max(opts.limit ?? 200, 1), 1e3);
|
|
24482
|
+
const got = await fetchIssues(resolved.repo, { state, limit }, deps);
|
|
24483
|
+
if (!got.ok) return { ok: false, kind: got.kind, error: got.error };
|
|
24484
|
+
const slug = resolved.repo.slug;
|
|
24485
|
+
const now = /* @__PURE__ */ new Date();
|
|
24486
|
+
let created = 0, updated = 0;
|
|
24487
|
+
for (const i of got.issues) {
|
|
24488
|
+
const id = issueRowId(projectId, slug, i.number);
|
|
24489
|
+
const exists = await prisma.githubIssue.findUnique({ where: { id }, select: { id: true } });
|
|
24490
|
+
const fresh = {
|
|
24491
|
+
title: i.title,
|
|
24492
|
+
body: i.body,
|
|
24493
|
+
authorLogin: i.authorLogin,
|
|
24494
|
+
labels: i.labels,
|
|
24495
|
+
url: i.url,
|
|
24496
|
+
issueState: i.issueState,
|
|
24497
|
+
issueCreatedAt: new Date(i.issueCreatedAt),
|
|
24498
|
+
issueUpdatedAt: new Date(i.issueUpdatedAt),
|
|
24499
|
+
pulledAt: now
|
|
24500
|
+
};
|
|
24501
|
+
await prisma.githubIssue.upsert({
|
|
24502
|
+
where: { id },
|
|
24503
|
+
create: { id, projectId, repoSlug: slug, number: i.number, status: "new", specId: null, ...fresh },
|
|
24504
|
+
update: fresh
|
|
24505
|
+
});
|
|
24506
|
+
if (exists) updated++;
|
|
24507
|
+
else created++;
|
|
24508
|
+
await notifySynced("githubIssue", id);
|
|
24509
|
+
}
|
|
24510
|
+
return {
|
|
24511
|
+
ok: true,
|
|
24512
|
+
repo: slug,
|
|
24513
|
+
pulled: got.issues.length,
|
|
24514
|
+
created,
|
|
24515
|
+
updated,
|
|
24516
|
+
via: got.via,
|
|
24517
|
+
skippedPullRequests: got.skippedPullRequests
|
|
24518
|
+
};
|
|
24519
|
+
}
|
|
24520
|
+
|
|
24521
|
+
// src/services/github-accept.ts
|
|
24522
|
+
init_src();
|
|
24523
|
+
init_db();
|
|
24524
|
+
var backlinkOf = (i) => `Dari GitHub issue ${i.repoSlug}#${i.number} (${i.url}).`;
|
|
24525
|
+
async function acceptGithubIssue(issue2, opts) {
|
|
24526
|
+
if (issue2.specId) {
|
|
24527
|
+
const spec2 = await prisma.spec.findUnique({ where: { id: issue2.specId } });
|
|
24528
|
+
if (spec2) return { spec: spec2, created: false };
|
|
24529
|
+
}
|
|
24530
|
+
const labels = Array.isArray(issue2.labels) ? issue2.labels : [];
|
|
24531
|
+
const source = opts.source ?? sourceForLabels(labels);
|
|
24532
|
+
const priority = opts.priority ?? "sedang";
|
|
24533
|
+
const backlink = backlinkOf(issue2);
|
|
24534
|
+
const detail = `${issue2.body}
|
|
24535
|
+
|
|
24536
|
+
Pelapor: @${issue2.authorLogin}
|
|
24537
|
+
Label: ${labels.length ? labels.join(", ") : "(tanpa label)"}
|
|
24538
|
+
${backlink}`;
|
|
24539
|
+
const payload = source === "qa" ? {
|
|
24540
|
+
severity: "major",
|
|
24541
|
+
steps: "Reproduksi dari deskripsi issue.",
|
|
24542
|
+
expected: "Perilaku yang diharapkan pelapor issue.",
|
|
24543
|
+
actual: detail,
|
|
24544
|
+
env: ""
|
|
24545
|
+
} : { context: detail, outcome: "", constraints: "", priority };
|
|
24546
|
+
const repoDir = await resolveRepoDir(issue2.projectId).catch(() => null);
|
|
24547
|
+
let spec = null;
|
|
24548
|
+
for (let attempt = 0; attempt < 3 && !spec; attempt++) {
|
|
24549
|
+
const sid = await nextSpecId(repoDir);
|
|
24550
|
+
try {
|
|
24551
|
+
spec = await prisma.spec.create({
|
|
24552
|
+
data: {
|
|
24553
|
+
id: sid,
|
|
24554
|
+
projectId: issue2.projectId,
|
|
24555
|
+
title: issue2.title,
|
|
24556
|
+
source,
|
|
24557
|
+
stage: "brainstorming",
|
|
24558
|
+
priority,
|
|
24559
|
+
author: `GitHub \xB7 ${opts.author}`,
|
|
24560
|
+
objective: `${issue2.title}. ${backlink}`,
|
|
24561
|
+
payload
|
|
24562
|
+
}
|
|
24563
|
+
});
|
|
24564
|
+
} catch (e) {
|
|
24565
|
+
if (e.code === "P2002" && attempt < 2) continue;
|
|
24566
|
+
throw e;
|
|
24567
|
+
}
|
|
24568
|
+
}
|
|
24569
|
+
await prisma.githubIssue.update({
|
|
24570
|
+
where: { id: issue2.id },
|
|
24571
|
+
data: { status: "accepted", specId: spec.id }
|
|
24572
|
+
});
|
|
24573
|
+
await notifySynced("spec", spec.id);
|
|
24574
|
+
await notifySynced("githubIssue", issue2.id);
|
|
24575
|
+
return { spec, created: true };
|
|
24576
|
+
}
|
|
24577
|
+
|
|
24578
|
+
// src/routes/github-issues.ts
|
|
24579
|
+
var zPull = external_exports.object({
|
|
24580
|
+
state: external_exports.enum(["open", "all"]).optional(),
|
|
24581
|
+
limit: external_exports.number().int().min(1).max(1e3).optional()
|
|
24582
|
+
}).default({});
|
|
24583
|
+
var zAccept = external_exports.object({
|
|
24584
|
+
priority: external_exports.enum(["tinggi", "sedang", "rendah"]).optional(),
|
|
24585
|
+
source: external_exports.enum(["qa", "brief", "audit"]).optional()
|
|
24586
|
+
}).default({});
|
|
24587
|
+
var zAcceptMany = external_exports.object({
|
|
24588
|
+
ids: external_exports.array(external_exports.string().min(1)).min(1).max(100),
|
|
24589
|
+
priority: external_exports.enum(["tinggi", "sedang", "rendah"]).optional(),
|
|
24590
|
+
source: external_exports.enum(["qa", "brief", "audit"]).optional()
|
|
24591
|
+
});
|
|
24592
|
+
var STATUS = {
|
|
24593
|
+
"no-project": 404,
|
|
24594
|
+
"not-found": 404,
|
|
24595
|
+
"no-remote": 400,
|
|
24596
|
+
"not-github": 400,
|
|
24597
|
+
"issues-disabled": 400,
|
|
24598
|
+
unauthorized: 401,
|
|
24599
|
+
other: 502
|
|
24600
|
+
};
|
|
24601
|
+
var view7 = (i) => ({
|
|
24602
|
+
id: i.id,
|
|
24603
|
+
projectId: i.projectId,
|
|
24604
|
+
repoSlug: i.repoSlug,
|
|
24605
|
+
number: i.number,
|
|
24606
|
+
title: i.title,
|
|
24607
|
+
body: i.body,
|
|
24608
|
+
authorLogin: i.authorLogin,
|
|
24609
|
+
labels: Array.isArray(i.labels) ? i.labels : [],
|
|
24610
|
+
url: i.url,
|
|
24611
|
+
issueState: i.issueState,
|
|
24612
|
+
status: i.status,
|
|
24613
|
+
specId: i.specId,
|
|
24614
|
+
issueCreatedAt: i.issueCreatedAt.toISOString(),
|
|
24615
|
+
issueUpdatedAt: i.issueUpdatedAt.toISOString(),
|
|
24616
|
+
pulledAt: i.pulledAt.toISOString()
|
|
24617
|
+
});
|
|
24618
|
+
async function githubIssues(app2) {
|
|
24619
|
+
app2.post("/projects/:id/github/pull", async (req, reply) => {
|
|
24620
|
+
const { id } = req.params;
|
|
24621
|
+
const parsed = zPull.safeParse(req.body ?? {});
|
|
24622
|
+
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
24623
|
+
const r = await pullIssues(id, parsed.data);
|
|
24624
|
+
if (!r.ok) return reply.code(STATUS[r.kind] ?? 400).send({ error: r.error });
|
|
24625
|
+
return reply.code(200).send(r);
|
|
24626
|
+
});
|
|
24627
|
+
app2.get("/projects/:id/github/issues", async (req, reply) => {
|
|
24628
|
+
const { id } = req.params;
|
|
24629
|
+
const { status } = req.query;
|
|
24630
|
+
const project = await prisma.project.findUnique({ where: { id }, select: { id: true } });
|
|
24631
|
+
if (!project) return reply.code(404).send({ error: "not found" });
|
|
24632
|
+
const items = await prisma.githubIssue.findMany({
|
|
24633
|
+
where: { projectId: id, ...status ? { status } : {} },
|
|
24634
|
+
orderBy: [{ number: "desc" }]
|
|
24635
|
+
});
|
|
24636
|
+
return reply.send({ items: items.map(view7) });
|
|
24637
|
+
});
|
|
24638
|
+
app2.post("/github-issues/accept", async (req, reply) => {
|
|
24639
|
+
const parsed = zAcceptMany.safeParse(req.body ?? {});
|
|
24640
|
+
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
24641
|
+
const { ids, priority, source } = parsed.data;
|
|
24642
|
+
const author = req.user?.email ?? "system";
|
|
24643
|
+
const created = [];
|
|
24644
|
+
const failed = [];
|
|
24645
|
+
for (const id of ids) {
|
|
24646
|
+
const issue2 = await prisma.githubIssue.findUnique({ where: { id } });
|
|
24647
|
+
if (!issue2) {
|
|
24648
|
+
failed.push({ id, error: "not found" });
|
|
24649
|
+
continue;
|
|
24650
|
+
}
|
|
24651
|
+
try {
|
|
24652
|
+
created.push((await acceptGithubIssue(issue2, { author, priority, source })).spec);
|
|
24653
|
+
} catch (e) {
|
|
24654
|
+
failed.push({ id, error: e.message });
|
|
24655
|
+
}
|
|
24656
|
+
}
|
|
24657
|
+
return reply.code(201).send({ created, failed });
|
|
24658
|
+
});
|
|
24659
|
+
app2.post("/github-issues/:id/accept", async (req, reply) => {
|
|
24660
|
+
const { id } = req.params;
|
|
24661
|
+
const parsed = zAccept.safeParse(req.body ?? {});
|
|
24662
|
+
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
24663
|
+
const issue2 = await prisma.githubIssue.findUnique({ where: { id } });
|
|
24664
|
+
if (!issue2) return reply.code(404).send({ error: "not found" });
|
|
24665
|
+
const { spec, created } = await acceptGithubIssue(issue2, {
|
|
24666
|
+
author: req.user?.email ?? "system",
|
|
24667
|
+
priority: parsed.data.priority,
|
|
24668
|
+
source: parsed.data.source
|
|
24669
|
+
});
|
|
24670
|
+
return reply.code(created ? 201 : 200).send(created ? { spec } : { spec, alreadyPromoted: true });
|
|
24671
|
+
});
|
|
24672
|
+
app2.post("/github-issues/:id/reject", async (req, reply) => {
|
|
24673
|
+
const { id } = req.params;
|
|
24674
|
+
const issue2 = await prisma.githubIssue.findUnique({ where: { id } });
|
|
24675
|
+
if (!issue2) return reply.code(404).send({ error: "not found" });
|
|
24676
|
+
const row = await prisma.githubIssue.update({ where: { id }, data: { status: "rejected" } });
|
|
24677
|
+
await notifySynced("githubIssue", id);
|
|
24678
|
+
return reply.send({ id: row.id, status: row.status });
|
|
24679
|
+
});
|
|
24680
|
+
app2.post("/github-issues/:id/unlink", async (req, reply) => {
|
|
24681
|
+
const { id } = req.params;
|
|
24682
|
+
const issue2 = await prisma.githubIssue.findUnique({ where: { id } });
|
|
24683
|
+
if (!issue2) return reply.code(404).send({ error: "not found" });
|
|
24684
|
+
const row = await prisma.githubIssue.update({ where: { id }, data: { status: "new", specId: null } });
|
|
24685
|
+
await notifySynced("githubIssue", id);
|
|
24686
|
+
return reply.send({ id: row.id, status: row.status, specId: row.specId });
|
|
24687
|
+
});
|
|
24688
|
+
}
|
|
24689
|
+
|
|
24690
|
+
// src/routes/telegram.ts
|
|
24691
|
+
init_zod();
|
|
24692
|
+
init_src();
|
|
24693
|
+
init_db();
|
|
24694
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
24695
|
+
|
|
24696
|
+
// src/services/telegram/runtime.ts
|
|
24697
|
+
var stoppedStatus = () => ({
|
|
24698
|
+
configured: false,
|
|
24699
|
+
enabled: false,
|
|
24700
|
+
running: false,
|
|
24701
|
+
readiness: "disabled",
|
|
24702
|
+
botUsername: null,
|
|
24703
|
+
allowlistCount: 0,
|
|
24704
|
+
agentTokenConfigured: false,
|
|
24705
|
+
missingCapabilities: [],
|
|
24706
|
+
lastUpdateAt: null,
|
|
24707
|
+
lastError: null
|
|
24708
|
+
});
|
|
24709
|
+
var registration = {
|
|
24710
|
+
agentTokenId: null,
|
|
24711
|
+
status: stoppedStatus()
|
|
24712
|
+
};
|
|
24713
|
+
var stopRuntime = null;
|
|
24714
|
+
function setTelegramRuntime(input) {
|
|
24715
|
+
registration = {
|
|
24716
|
+
agentTokenId: input.agentTokenId === void 0 ? registration.agentTokenId : input.agentTokenId,
|
|
24717
|
+
status: input.status ?? registration.status
|
|
24718
|
+
};
|
|
24719
|
+
}
|
|
24720
|
+
function updateTelegramRuntimeStatus(patch) {
|
|
24721
|
+
registration = { ...registration, status: { ...registration.status, ...patch } };
|
|
24722
|
+
}
|
|
24723
|
+
function telegramRuntimeStatus() {
|
|
24724
|
+
return { ...registration.status };
|
|
24725
|
+
}
|
|
24726
|
+
function telegramGatewayAgentTokenId() {
|
|
24727
|
+
return registration.agentTokenId;
|
|
24728
|
+
}
|
|
24729
|
+
function registerTelegramRuntimeStop(stop) {
|
|
24730
|
+
stopRuntime = stop;
|
|
24731
|
+
}
|
|
24732
|
+
async function stopTelegramRuntime() {
|
|
24733
|
+
const stop = stopRuntime;
|
|
24734
|
+
stopRuntime = null;
|
|
24735
|
+
if (stop) await stop();
|
|
24736
|
+
}
|
|
24737
|
+
|
|
24738
|
+
// src/services/telegram/store.ts
|
|
24739
|
+
import { Prisma } from "@prisma/client";
|
|
24740
|
+
var isUnique = (error) => error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002";
|
|
24741
|
+
var TelegramStore = class {
|
|
24742
|
+
constructor(db) {
|
|
24743
|
+
this.db = db;
|
|
24744
|
+
}
|
|
24745
|
+
async recordUpdate(input) {
|
|
24746
|
+
try {
|
|
24747
|
+
await this.db.$transaction(async (tx) => {
|
|
24748
|
+
await tx.telegramUpdate.create({ data: {
|
|
24749
|
+
updateId: input.updateId,
|
|
24750
|
+
chatId: input.chatId ?? null,
|
|
24751
|
+
userId: input.userId ?? null,
|
|
24752
|
+
kind: input.kind,
|
|
24753
|
+
digest: input.digest
|
|
24754
|
+
} });
|
|
24755
|
+
const current = await tx.telegramGatewayState.findUnique({ where: { id: 1 }, select: { offset: true } });
|
|
24756
|
+
const offset = Math.max(current?.offset ?? 0, input.updateId + 1);
|
|
24757
|
+
await tx.telegramGatewayState.upsert({
|
|
24758
|
+
where: { id: 1 },
|
|
24759
|
+
create: { id: 1, offset, lastUpdateAt: /* @__PURE__ */ new Date() },
|
|
24760
|
+
update: { offset, lastUpdateAt: /* @__PURE__ */ new Date() }
|
|
24761
|
+
});
|
|
24762
|
+
});
|
|
24763
|
+
return true;
|
|
24764
|
+
} catch (error) {
|
|
24765
|
+
if (isUnique(error)) return false;
|
|
24766
|
+
throw error;
|
|
24767
|
+
}
|
|
24768
|
+
}
|
|
24769
|
+
async offset() {
|
|
24770
|
+
return (await this.db.telegramGatewayState.findUnique({ where: { id: 1 } }))?.offset ?? 0;
|
|
24771
|
+
}
|
|
24772
|
+
async claimUpdate(updateId) {
|
|
24773
|
+
return (await this.db.telegramUpdate.updateMany({
|
|
24774
|
+
where: { updateId, state: "received" },
|
|
24775
|
+
data: { state: "dispatching", claimedAt: /* @__PURE__ */ new Date() }
|
|
24776
|
+
})).count === 1;
|
|
24777
|
+
}
|
|
24778
|
+
async markDispatched(updateId) {
|
|
24779
|
+
await this.db.telegramUpdate.updateMany({
|
|
24780
|
+
where: { updateId, state: "dispatching" },
|
|
24781
|
+
data: { state: "dispatched", dispatchedAt: /* @__PURE__ */ new Date() }
|
|
24782
|
+
});
|
|
24783
|
+
}
|
|
24784
|
+
async rejectUpdate(updateId, reason) {
|
|
24785
|
+
return (await this.db.telegramUpdate.updateMany({
|
|
24786
|
+
where: { updateId, state: "received" },
|
|
24787
|
+
data: { state: "rejected", rejectReason: reason }
|
|
24788
|
+
})).count === 1;
|
|
24789
|
+
}
|
|
24790
|
+
async recoverUncertainClaims() {
|
|
24791
|
+
return (await this.db.telegramUpdate.updateMany({
|
|
24792
|
+
where: { state: { in: ["received", "dispatching"] } },
|
|
24793
|
+
data: { state: "uncertain" }
|
|
24794
|
+
})).count;
|
|
24795
|
+
}
|
|
24796
|
+
async ensureChat(input) {
|
|
24797
|
+
return this.db.telegramChat.upsert({
|
|
24798
|
+
where: { chatId: input.chatId },
|
|
24799
|
+
create: input,
|
|
24800
|
+
update: { userId: input.userId }
|
|
24801
|
+
});
|
|
24802
|
+
}
|
|
24803
|
+
async chatContext(chatId) {
|
|
24804
|
+
const chat = await this.db.telegramChat.findUnique({ where: { chatId } });
|
|
24805
|
+
if (!chat) return null;
|
|
24806
|
+
const memories = await this.db.telegramMemory.findMany({ where: { chatId }, orderBy: { createdAt: "asc" } });
|
|
24807
|
+
return {
|
|
24808
|
+
chatId: chat.chatId,
|
|
24809
|
+
userId: chat.userId,
|
|
24810
|
+
sessionId: chat.sessionId,
|
|
24811
|
+
activeProjectId: chat.activeProjectId,
|
|
24812
|
+
activeSessionId: chat.activeSessionId,
|
|
24813
|
+
personalityAgentId: chat.personalityAgentId,
|
|
24814
|
+
summary: chat.summary,
|
|
24815
|
+
agent: chat.agent,
|
|
24816
|
+
model: chat.model,
|
|
24817
|
+
effort: chat.effort,
|
|
24818
|
+
memories: memories.map(this.memoryView)
|
|
24819
|
+
};
|
|
24820
|
+
}
|
|
24821
|
+
patchChat(chatId, patch) {
|
|
24822
|
+
return this.db.telegramChat.update({ where: { chatId }, data: patch });
|
|
24823
|
+
}
|
|
24824
|
+
async bindSession(chatId, sessionId2) {
|
|
24825
|
+
await this.db.telegramChat.update({ where: { chatId }, data: { sessionId: sessionId2 } });
|
|
24826
|
+
}
|
|
24827
|
+
async rateLimitExceeded(userId, since, limit) {
|
|
24828
|
+
return await this.db.telegramUpdate.count({ where: { userId, receivedAt: { gte: since } } }) > limit;
|
|
24829
|
+
}
|
|
24830
|
+
async markUpdateUncertain(updateId, reason) {
|
|
24831
|
+
await this.db.telegramUpdate.updateMany({
|
|
24832
|
+
where: { updateId, state: "dispatching" },
|
|
24833
|
+
data: { state: "uncertain", rejectReason: reason }
|
|
24834
|
+
});
|
|
24835
|
+
}
|
|
24836
|
+
async addMemory(chatId, content) {
|
|
24837
|
+
return this.memoryView(await this.db.telegramMemory.create({ data: { chatId, content } }));
|
|
24838
|
+
}
|
|
24839
|
+
async forgetMemory(chatId, id) {
|
|
24840
|
+
return (await this.db.telegramMemory.deleteMany({ where: { id, chatId } })).count === 1;
|
|
24841
|
+
}
|
|
24842
|
+
async resetMemory(chatId) {
|
|
24843
|
+
const chat = await this.db.telegramChat.findUnique({ where: { chatId }, select: { chatId: true } });
|
|
24844
|
+
if (!chat) return false;
|
|
24845
|
+
await this.db.$transaction([
|
|
24846
|
+
this.db.telegramMemory.deleteMany({ where: { chatId } }),
|
|
24847
|
+
this.db.telegramChat.update({ where: { chatId }, data: { summary: null } })
|
|
24848
|
+
]);
|
|
24849
|
+
return true;
|
|
24850
|
+
}
|
|
24851
|
+
memoryView(row) {
|
|
24852
|
+
return { ...row, createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString() };
|
|
24853
|
+
}
|
|
24854
|
+
async enqueueReply(input) {
|
|
24855
|
+
const dedupeKey = `${input.chatId}:${input.updateId}:${input.kind}`;
|
|
24856
|
+
try {
|
|
24857
|
+
return await this.db.$transaction(async (tx) => {
|
|
24858
|
+
const existing = await tx.telegramOutbox.findUnique({ where: { dedupeKey } });
|
|
24859
|
+
if (existing) return existing;
|
|
24860
|
+
let confirmationId = null;
|
|
24861
|
+
if (input.confirmation) {
|
|
24862
|
+
const confirmation = await tx.telegramConfirmation.create({ data: {
|
|
24863
|
+
callbackToken: input.confirmation.callbackToken,
|
|
24864
|
+
chatId: input.chatId,
|
|
24865
|
+
userId: input.confirmation.userId,
|
|
24866
|
+
updateId: input.updateId,
|
|
24867
|
+
description: input.confirmation.description,
|
|
24868
|
+
method: input.confirmation.method,
|
|
24869
|
+
path: input.confirmation.path,
|
|
24870
|
+
expiresAt: input.confirmation.expiresAt
|
|
24871
|
+
} });
|
|
24872
|
+
confirmationId = confirmation.id;
|
|
24873
|
+
}
|
|
24874
|
+
return tx.telegramOutbox.create({ data: {
|
|
24875
|
+
chatId: input.chatId,
|
|
24876
|
+
updateId: input.updateId,
|
|
24877
|
+
kind: input.kind,
|
|
24878
|
+
dedupeKey,
|
|
24879
|
+
text: input.text,
|
|
24880
|
+
confirmationId
|
|
24881
|
+
} });
|
|
24882
|
+
});
|
|
24883
|
+
} catch (error) {
|
|
24884
|
+
if (!isUnique(error)) throw error;
|
|
24885
|
+
return await this.db.telegramOutbox.findUnique({ where: { dedupeKey } });
|
|
24886
|
+
}
|
|
24887
|
+
}
|
|
24888
|
+
async claimNextOutbox() {
|
|
24889
|
+
for (; ; ) {
|
|
24890
|
+
const row = await this.db.telegramOutbox.findFirst({ where: { state: "pending" }, orderBy: { createdAt: "asc" } });
|
|
24891
|
+
if (!row) return null;
|
|
24892
|
+
const claimed = await this.db.telegramOutbox.updateMany({
|
|
24893
|
+
where: { id: row.id, state: "pending" },
|
|
24894
|
+
data: { state: "sending", claimedAt: /* @__PURE__ */ new Date() }
|
|
24895
|
+
});
|
|
24896
|
+
if (claimed.count === 1) return this.db.telegramOutbox.findUnique({ where: { id: row.id } });
|
|
24897
|
+
}
|
|
24898
|
+
}
|
|
24899
|
+
async recoverUncertainOutbox() {
|
|
24900
|
+
return (await this.db.telegramOutbox.updateMany({ where: { state: "sending" }, data: { state: "uncertain" } })).count;
|
|
24901
|
+
}
|
|
24902
|
+
markOutboxSent(id, telegramMessageId) {
|
|
24903
|
+
return this.db.telegramOutbox.updateMany({
|
|
24904
|
+
where: { id, state: "sending" },
|
|
24905
|
+
data: { state: "sent", telegramMessageId, sentAt: /* @__PURE__ */ new Date() }
|
|
24906
|
+
});
|
|
24907
|
+
}
|
|
24908
|
+
markOutboxFailed(id, error) {
|
|
24909
|
+
return this.db.telegramOutbox.updateMany({ where: { id, state: "sending" }, data: { state: "failed", error } });
|
|
24910
|
+
}
|
|
24911
|
+
markOutboxUncertain(id, error) {
|
|
24912
|
+
return this.db.telegramOutbox.updateMany({ where: { id, state: "sending" }, data: { state: "uncertain", error } });
|
|
24913
|
+
}
|
|
24914
|
+
confirmation(id) {
|
|
24915
|
+
return this.db.telegramConfirmation.findUnique({ where: { id } });
|
|
24916
|
+
}
|
|
24917
|
+
async resolveConfirmation(input) {
|
|
24918
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
24919
|
+
const row = await this.db.telegramConfirmation.findUnique({ where: { callbackToken: input.callbackToken } });
|
|
24920
|
+
if (!row || row.chatId !== input.chatId || row.userId !== input.userId || row.state !== "pending") return "invalid";
|
|
24921
|
+
if (row.expiresAt <= now) {
|
|
24922
|
+
await this.db.telegramConfirmation.updateMany({
|
|
24923
|
+
where: { id: row.id, state: "pending" },
|
|
24924
|
+
data: { state: "expired" }
|
|
24925
|
+
});
|
|
24926
|
+
return "expired";
|
|
24927
|
+
}
|
|
24928
|
+
const state = input.action === "approve" ? "approved" : "denied";
|
|
24929
|
+
const changed = await this.db.telegramConfirmation.updateMany({
|
|
24930
|
+
where: { id: row.id, state: "pending" },
|
|
24931
|
+
data: { state, ...state === "approved" ? { approvedAt: now } : {} }
|
|
24932
|
+
});
|
|
24933
|
+
return changed.count === 1 ? state : "invalid";
|
|
24934
|
+
}
|
|
24935
|
+
async audit(input) {
|
|
24936
|
+
const row = await this.db.telegramAudit.create({ data: {
|
|
24937
|
+
chatId: input.chatId ?? null,
|
|
24938
|
+
userId: input.userId ?? null,
|
|
24939
|
+
updateId: input.updateId ?? null,
|
|
24940
|
+
action: input.action,
|
|
24941
|
+
outcome: input.outcome,
|
|
24942
|
+
correlationId: input.correlationId ?? null,
|
|
24943
|
+
method: input.method ?? null,
|
|
24944
|
+
path: input.path ?? null,
|
|
24945
|
+
statusCode: input.statusCode ?? null
|
|
24946
|
+
} });
|
|
24947
|
+
return { ...row, createdAt: row.createdAt.toISOString() };
|
|
24948
|
+
}
|
|
24949
|
+
async listAudit(input) {
|
|
24950
|
+
const where = {
|
|
24951
|
+
...input.chatId ? { chatId: input.chatId } : {},
|
|
24952
|
+
...input.updateId !== void 0 ? { updateId: input.updateId } : {}
|
|
24953
|
+
};
|
|
24954
|
+
const [rows, total] = await this.db.$transaction([
|
|
24955
|
+
this.db.telegramAudit.findMany({ where, orderBy: { createdAt: "desc" }, take: input.take, skip: input.skip }),
|
|
24956
|
+
this.db.telegramAudit.count({ where })
|
|
24957
|
+
]);
|
|
24958
|
+
return { items: rows.map((row) => ({ ...row, createdAt: row.createdAt.toISOString() })), total };
|
|
24959
|
+
}
|
|
24960
|
+
};
|
|
24961
|
+
|
|
24962
|
+
// src/routes/telegram.ts
|
|
24963
|
+
var store = new TelegramStore(prisma);
|
|
24964
|
+
var zContextPatch = external_exports.object({
|
|
24965
|
+
activeProjectId: external_exports.string().min(1).nullable().optional(),
|
|
24966
|
+
activeSessionId: external_exports.string().min(1).nullable().optional(),
|
|
24967
|
+
personalityAgentId: external_exports.string().min(1).nullable().optional(),
|
|
24968
|
+
summary: external_exports.string().trim().min(1).max(4e3).nullable().optional()
|
|
24969
|
+
}).refine((value) => Object.keys(value).length > 0, "empty patch");
|
|
24970
|
+
var zMemory = external_exports.object({ content: external_exports.string().trim().min(1).max(1e3) });
|
|
24971
|
+
async function telegramRoutes(app2) {
|
|
24972
|
+
app2.get("/telegram/status", async () => telegramRuntimeStatus());
|
|
24973
|
+
app2.get("/telegram/chats/:chatId/context", async (req, reply) => {
|
|
24974
|
+
const context = await store.chatContext(req.params.chatId);
|
|
24975
|
+
return context ?? reply.code(404).send({ error: "chat not found" });
|
|
24976
|
+
});
|
|
24977
|
+
app2.patch("/telegram/chats/:chatId/context", async (req, reply) => {
|
|
24978
|
+
const parsed = zContextPatch.safeParse(req.body);
|
|
24979
|
+
if (!parsed.success) return reply.code(400).send({ error: "invalid body" });
|
|
24980
|
+
const chatId = req.params.chatId;
|
|
24981
|
+
try {
|
|
24982
|
+
await store.patchChat(chatId, parsed.data);
|
|
24983
|
+
} catch {
|
|
24984
|
+
return reply.code(404).send({ error: "chat not found" });
|
|
24985
|
+
}
|
|
24986
|
+
return store.chatContext(chatId);
|
|
24987
|
+
});
|
|
24988
|
+
app2.post("/telegram/chats/:chatId/memories", async (req, reply) => {
|
|
24989
|
+
const parsed = zMemory.safeParse(req.body);
|
|
24990
|
+
if (!parsed.success) return reply.code(400).send({ error: "invalid body" });
|
|
24991
|
+
const chatId = req.params.chatId;
|
|
24992
|
+
if (!await prisma.telegramChat.findUnique({ where: { chatId }, select: { chatId: true } })) {
|
|
24993
|
+
return reply.code(404).send({ error: "chat not found" });
|
|
24994
|
+
}
|
|
24995
|
+
return reply.code(201).send(await store.addMemory(chatId, parsed.data.content));
|
|
24996
|
+
});
|
|
24997
|
+
app2.delete("/telegram/chats/:chatId/memories/:id", async (req, reply) => {
|
|
24998
|
+
const { chatId, id } = req.params;
|
|
24999
|
+
if (!await store.forgetMemory(chatId, id)) return reply.code(404).send({ error: "memory not found" });
|
|
25000
|
+
return reply.code(204).send();
|
|
25001
|
+
});
|
|
25002
|
+
app2.delete("/telegram/chats/:chatId/memories", async (req, reply) => {
|
|
25003
|
+
if (!await store.resetMemory(req.params.chatId)) {
|
|
25004
|
+
return reply.code(404).send({ error: "chat not found" });
|
|
25005
|
+
}
|
|
25006
|
+
return reply.code(204).send();
|
|
25007
|
+
});
|
|
25008
|
+
app2.post("/telegram/replies", async (req, reply) => {
|
|
25009
|
+
const parsed = zTelegramReplyInput.safeParse(req.body);
|
|
25010
|
+
if (!parsed.success) return reply.code(400).send({ error: "invalid body" });
|
|
25011
|
+
const input = parsed.data;
|
|
25012
|
+
const update2 = await prisma.telegramUpdate.findFirst({
|
|
25013
|
+
where: { updateId: input.updateId, chatId: input.chatId, state: { in: ["dispatching", "dispatched"] } }
|
|
25014
|
+
});
|
|
25015
|
+
if (!update2?.userId) return reply.code(409).send({ error: "update/chat correlation invalid" });
|
|
25016
|
+
const outbox = await store.enqueueReply({
|
|
25017
|
+
chatId: input.chatId,
|
|
25018
|
+
updateId: input.updateId,
|
|
25019
|
+
kind: input.kind,
|
|
25020
|
+
text: input.text,
|
|
25021
|
+
...input.confirmation ? { confirmation: {
|
|
25022
|
+
callbackToken: randomBytes3(12).toString("base64url"),
|
|
25023
|
+
userId: update2.userId,
|
|
25024
|
+
description: input.confirmation.description,
|
|
25025
|
+
method: input.confirmation.method,
|
|
25026
|
+
path: input.confirmation.path,
|
|
25027
|
+
expiresAt: new Date(Date.now() + 10 * 6e4)
|
|
25028
|
+
} } : {}
|
|
25029
|
+
});
|
|
25030
|
+
if (input.summary !== void 0) await store.patchChat(input.chatId, { summary: input.summary });
|
|
25031
|
+
for (const memory of input.remember ?? []) await store.addMemory(input.chatId, memory);
|
|
25032
|
+
await store.audit({
|
|
25033
|
+
chatId: input.chatId,
|
|
25034
|
+
userId: update2.userId,
|
|
25035
|
+
updateId: input.updateId,
|
|
25036
|
+
action: "reply-enqueue",
|
|
25037
|
+
outcome: input.kind,
|
|
25038
|
+
correlationId: `tg:${input.updateId}`
|
|
25039
|
+
});
|
|
25040
|
+
return reply.code(202).send({ id: outbox.id, state: outbox.state });
|
|
25041
|
+
});
|
|
25042
|
+
app2.get("/telegram/audit", async (req, reply) => {
|
|
25043
|
+
const parsed = external_exports.object({
|
|
25044
|
+
chatId: external_exports.string().optional(),
|
|
25045
|
+
updateId: external_exports.coerce.number().int().nonnegative().optional(),
|
|
25046
|
+
take: external_exports.coerce.number().int().min(1).max(100).default(50),
|
|
25047
|
+
skip: external_exports.coerce.number().int().min(0).default(0)
|
|
25048
|
+
}).safeParse(req.query);
|
|
25049
|
+
if (!parsed.success) return reply.code(400).send({ error: "invalid query" });
|
|
25050
|
+
return store.listAudit(parsed.data);
|
|
25051
|
+
});
|
|
25052
|
+
}
|
|
25053
|
+
|
|
25054
|
+
// src/app.ts
|
|
25055
|
+
var import_multipart = __toESM(require_multipart2(), 1);
|
|
25056
|
+
|
|
25057
|
+
// src/routes/auth.ts
|
|
25058
|
+
init_src();
|
|
25059
|
+
init_db();
|
|
25060
|
+
|
|
25061
|
+
// src/services/auth.ts
|
|
25062
|
+
init_db();
|
|
25063
|
+
import { randomBytes as randomBytes4, scrypt as scryptCb, timingSafeEqual, createHash as createHash3 } from "node:crypto";
|
|
25064
|
+
import { promisify as promisify9 } from "node:util";
|
|
25065
|
+
var scrypt = promisify9(scryptCb);
|
|
25066
|
+
var COOKIE_NAME = "hn_session";
|
|
25067
|
+
var SESSION_TTL_MS = 7 * 24 * 3600 * 1e3;
|
|
25068
|
+
async function hashPassword(pw) {
|
|
25069
|
+
const salt = randomBytes4(16);
|
|
25070
|
+
const key = await scrypt(pw, salt, 64);
|
|
25071
|
+
return `${salt.toString("hex")}:${key.toString("hex")}`;
|
|
25072
|
+
}
|
|
25073
|
+
async function verifyPassword(pw, stored) {
|
|
25074
|
+
const [saltHex, hashHex] = stored.split(":");
|
|
25075
|
+
if (!saltHex || !hashHex) return false;
|
|
25076
|
+
const key = await scrypt(pw, Buffer.from(saltHex, "hex"), 64);
|
|
25077
|
+
const want = Buffer.from(hashHex, "hex");
|
|
25078
|
+
return key.length === want.length && timingSafeEqual(key, want);
|
|
25079
|
+
}
|
|
25080
|
+
var newSessionToken = () => randomBytes4(32).toString("base64url");
|
|
25081
|
+
var sessionId = (token) => createHash3("sha256").update(token).digest("hex");
|
|
25082
|
+
async function createSession2(userId) {
|
|
25083
|
+
const token = newSessionToken();
|
|
25084
|
+
await prisma.session.create({
|
|
25085
|
+
data: { id: sessionId(token), userId, expiresAt: new Date(Date.now() + SESSION_TTL_MS) }
|
|
25086
|
+
});
|
|
25087
|
+
return token;
|
|
25088
|
+
}
|
|
25089
|
+
async function lookupSession(token) {
|
|
25090
|
+
const s = await prisma.session.findUnique({ where: { id: sessionId(token) }, include: { user: true } });
|
|
25091
|
+
if (!s) return null;
|
|
25092
|
+
if (s.expiresAt.getTime() < Date.now()) {
|
|
25093
|
+
await prisma.session.delete({ where: { id: s.id } }).catch(() => {
|
|
25094
|
+
});
|
|
25095
|
+
return null;
|
|
25096
|
+
}
|
|
25097
|
+
return { id: s.user.id, email: s.user.email, createdAt: s.user.createdAt.toISOString() };
|
|
25098
|
+
}
|
|
25099
|
+
async function deleteSession(token) {
|
|
25100
|
+
await prisma.session.deleteMany({ where: { id: sessionId(token) } });
|
|
25101
|
+
}
|
|
25102
|
+
async function deleteUserSessions(userId, exceptToken) {
|
|
25103
|
+
await prisma.session.deleteMany({
|
|
25104
|
+
where: { userId, ...exceptToken ? { id: { not: sessionId(exceptToken) } } : {} }
|
|
25105
|
+
});
|
|
25106
|
+
}
|
|
25107
|
+
var fails = /* @__PURE__ */ new Map();
|
|
23772
25108
|
function loginThrottle(ip) {
|
|
23773
25109
|
const f = fails.get(ip);
|
|
23774
25110
|
return { blocked: !!f && f.until > Date.now() };
|
|
@@ -23796,7 +25132,7 @@ function cookieOpts() {
|
|
|
23796
25132
|
}
|
|
23797
25133
|
|
|
23798
25134
|
// src/routes/auth.ts
|
|
23799
|
-
var
|
|
25135
|
+
var view8 = (u) => ({ id: u.id, email: u.email, createdAt: u.createdAt.toISOString() });
|
|
23800
25136
|
async function issue(reply, userId) {
|
|
23801
25137
|
const token = await createSession2(userId);
|
|
23802
25138
|
reply.setCookie(COOKIE_NAME, token, cookieOpts());
|
|
@@ -23814,7 +25150,7 @@ async function auth_default(app2) {
|
|
|
23814
25150
|
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
|
|
23815
25151
|
});
|
|
23816
25152
|
await issue(reply, user.id);
|
|
23817
|
-
return { user:
|
|
25153
|
+
return { user: view8(user) };
|
|
23818
25154
|
});
|
|
23819
25155
|
app2.post("/auth/login", async (req, reply) => {
|
|
23820
25156
|
if (loginThrottle(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
|
|
@@ -23827,7 +25163,7 @@ async function auth_default(app2) {
|
|
|
23827
25163
|
}
|
|
23828
25164
|
clearLoginFails(req.ip);
|
|
23829
25165
|
await issue(reply, user.id);
|
|
23830
|
-
return { user:
|
|
25166
|
+
return { user: view8(user) };
|
|
23831
25167
|
});
|
|
23832
25168
|
app2.post("/auth/logout", async (req, reply) => {
|
|
23833
25169
|
const token = req.cookies?.[COOKIE_NAME];
|
|
@@ -23835,7 +25171,7 @@ async function auth_default(app2) {
|
|
|
23835
25171
|
reply.clearCookie(COOKIE_NAME, { path: "/" });
|
|
23836
25172
|
return reply.code(204).send();
|
|
23837
25173
|
});
|
|
23838
|
-
app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(
|
|
25174
|
+
app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view8));
|
|
23839
25175
|
app2.post("/auth/users", async (req, reply) => {
|
|
23840
25176
|
const p = zSignup.safeParse(req.body);
|
|
23841
25177
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
@@ -23844,7 +25180,7 @@ async function auth_default(app2) {
|
|
|
23844
25180
|
const user = await prisma.user.create({
|
|
23845
25181
|
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
|
|
23846
25182
|
});
|
|
23847
|
-
return
|
|
25183
|
+
return view8(user);
|
|
23848
25184
|
});
|
|
23849
25185
|
app2.delete("/auth/users/:id", async (req, reply) => {
|
|
23850
25186
|
if (await prisma.user.count() <= 1) return reply.code(400).send({ error: "tak bisa hapus user terakhir" });
|
|
@@ -23864,7 +25200,7 @@ async function auth_default(app2) {
|
|
|
23864
25200
|
});
|
|
23865
25201
|
await deleteUserSessions(user.id);
|
|
23866
25202
|
await issue(reply, user.id);
|
|
23867
|
-
return { user:
|
|
25203
|
+
return { user: view8(user) };
|
|
23868
25204
|
});
|
|
23869
25205
|
}
|
|
23870
25206
|
|
|
@@ -23873,7 +25209,7 @@ init_src();
|
|
|
23873
25209
|
|
|
23874
25210
|
// src/services/agent-token.ts
|
|
23875
25211
|
init_db();
|
|
23876
|
-
import { randomBytes as
|
|
25212
|
+
import { randomBytes as randomBytes5, createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
23877
25213
|
var hash = (token) => createHash4("sha256").update(token).digest("hex");
|
|
23878
25214
|
function toAgentTokenView(t) {
|
|
23879
25215
|
return {
|
|
@@ -23889,7 +25225,7 @@ function toAgentTokenView(t) {
|
|
|
23889
25225
|
};
|
|
23890
25226
|
}
|
|
23891
25227
|
async function issueAgentToken(input) {
|
|
23892
|
-
const token = "hnm_agt_" +
|
|
25228
|
+
const token = "hnm_agt_" + randomBytes5(24).toString("hex");
|
|
23893
25229
|
const row = await prisma.agentToken.create({
|
|
23894
25230
|
data: {
|
|
23895
25231
|
name: input.name,
|
|
@@ -23943,15 +25279,15 @@ async function agent_tokens_default(app2) {
|
|
|
23943
25279
|
app2.post("/agent-tokens", async (req, reply) => {
|
|
23944
25280
|
const p = zAgentTokenCreate.safeParse(req.body);
|
|
23945
25281
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
23946
|
-
const { view:
|
|
23947
|
-
return reply.code(201).send({ ...
|
|
25282
|
+
const { view: view9, token } = await issueAgentToken({ ...p.data, createdBy: req.user?.id });
|
|
25283
|
+
return reply.code(201).send({ ...view9, token });
|
|
23948
25284
|
});
|
|
23949
25285
|
app2.patch("/agent-tokens/:id", async (req, reply) => {
|
|
23950
25286
|
const p = zAgentTokenPatch.safeParse(req.body);
|
|
23951
25287
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
23952
25288
|
const { id } = req.params;
|
|
23953
|
-
const
|
|
23954
|
-
return
|
|
25289
|
+
const view9 = await patchAgentToken(id, p.data);
|
|
25290
|
+
return view9 ? reply.send(view9) : reply.code(404).send({ error: "not found" });
|
|
23955
25291
|
});
|
|
23956
25292
|
app2.delete("/agent-tokens/:id", async (req, reply) => {
|
|
23957
25293
|
const { id } = req.params;
|
|
@@ -24000,10 +25336,12 @@ function capabilityForRoute(method, path) {
|
|
|
24000
25336
|
if (top === "scheduler") return rw("settings");
|
|
24001
25337
|
if (top === "lead") return rw("lead");
|
|
24002
25338
|
if (top === "custom-agents") return rw("agents");
|
|
25339
|
+
if (top === "telegram") return rw("telegram");
|
|
24003
25340
|
if (top === "settings" || top === "config") return rw("settings");
|
|
24004
25341
|
if (top === "specs") return rw("backlog");
|
|
24005
25342
|
if (top === "notifications") return rw("notifications");
|
|
24006
25343
|
if (top === "tickets") return rw("support");
|
|
25344
|
+
if (top === "github-issues") return rw("support");
|
|
24007
25345
|
if (top === "vps") return rw("vps");
|
|
24008
25346
|
if (top === "prds") return rw("docs");
|
|
24009
25347
|
if (top === "terminal") {
|
|
@@ -24013,6 +25351,7 @@ function capabilityForRoute(method, path) {
|
|
|
24013
25351
|
if (top === "projects") {
|
|
24014
25352
|
const sub = seg[2];
|
|
24015
25353
|
if (sub === "docs" || sub === "prds") return rw("docs");
|
|
25354
|
+
if (sub === "github") return rw("support");
|
|
24016
25355
|
if (sub && IDE_SUBS.has(sub)) return rw("ide");
|
|
24017
25356
|
return rw("projects");
|
|
24018
25357
|
}
|
|
@@ -24028,6 +25367,119 @@ function checkAgentCapability(caps, method, path) {
|
|
|
24028
25367
|
|
|
24029
25368
|
// src/app.ts
|
|
24030
25369
|
init_pty();
|
|
25370
|
+
|
|
25371
|
+
// src/services/telegram/security.ts
|
|
25372
|
+
init_db();
|
|
25373
|
+
var store2 = new TelegramStore(prisma);
|
|
25374
|
+
function isDestructiveTelegramRequest(method, path, body) {
|
|
25375
|
+
const verb = method.toUpperCase();
|
|
25376
|
+
if (verb === "DELETE") return true;
|
|
25377
|
+
if (verb === "POST") {
|
|
25378
|
+
if (/\/update\/apply$/.test(path)) return true;
|
|
25379
|
+
if (/\/(?:specs|terminal\/sessions)\/[^/]+\/integrate$/.test(path)) return true;
|
|
25380
|
+
if (/\/projects\/[^/]+\/(?:git\/(?:merge|rebase|drop|reset|clean)|branches\/delete)$/.test(path)) return true;
|
|
25381
|
+
if (/\/projects\/[^/]+\/git$/.test(path)) {
|
|
25382
|
+
const data = typeof body === "object" && body !== null ? body : {};
|
|
25383
|
+
return (/* @__PURE__ */ new Set([
|
|
25384
|
+
"merge",
|
|
25385
|
+
"cherry-pick",
|
|
25386
|
+
"revert",
|
|
25387
|
+
"delete-branch",
|
|
25388
|
+
"reset",
|
|
25389
|
+
"delete-tag",
|
|
25390
|
+
"reset-worktree",
|
|
25391
|
+
"clean",
|
|
25392
|
+
"stash-pop",
|
|
25393
|
+
"stash-drop"
|
|
25394
|
+
])).has(String(data.op ?? ""));
|
|
25395
|
+
}
|
|
25396
|
+
if (/\/vps\/[^/]+\/(?:harden|remediate)$/.test(path)) return true;
|
|
25397
|
+
}
|
|
25398
|
+
if (verb === "PATCH" && /\/specs\/[^/]+$/.test(path)) {
|
|
25399
|
+
const data = typeof body === "object" && body !== null ? body : {};
|
|
25400
|
+
return typeof data.stage === "string" && data.confirmDelete === true;
|
|
25401
|
+
}
|
|
25402
|
+
return false;
|
|
25403
|
+
}
|
|
25404
|
+
var header = (req, name2) => {
|
|
25405
|
+
const value = req.headers[name2];
|
|
25406
|
+
return typeof value === "string" && value ? value : null;
|
|
25407
|
+
};
|
|
25408
|
+
async function reject(req, reply, message, updateId) {
|
|
25409
|
+
const path = req.url.split("?")[0] ?? req.url;
|
|
25410
|
+
await store2.audit({
|
|
25411
|
+
updateId: updateId ?? null,
|
|
25412
|
+
action: "api-guard",
|
|
25413
|
+
outcome: `rejected:${message}`,
|
|
25414
|
+
correlationId: updateId === void 0 ? null : `tg:${updateId}`,
|
|
25415
|
+
method: req.method,
|
|
25416
|
+
path,
|
|
25417
|
+
statusCode: 403
|
|
25418
|
+
}).catch(() => {
|
|
25419
|
+
});
|
|
25420
|
+
await reply.code(403).send({ error: message });
|
|
25421
|
+
}
|
|
25422
|
+
async function guardTelegramGatewayRequest(req, reply) {
|
|
25423
|
+
const gatewayId = telegramGatewayAgentTokenId();
|
|
25424
|
+
if (!gatewayId || req.agent?.id !== gatewayId) return;
|
|
25425
|
+
const correlation = header(req, "x-hanoman-telegram-update");
|
|
25426
|
+
const updateId = correlation && /^\d+$/.test(correlation) ? Number(correlation) : null;
|
|
25427
|
+
if (updateId === null || !Number.isSafeInteger(updateId)) {
|
|
25428
|
+
return reject(req, reply, "telegram correlation required");
|
|
25429
|
+
}
|
|
25430
|
+
const update2 = await prisma.telegramUpdate.findUnique({ where: { updateId } });
|
|
25431
|
+
if (!update2?.chatId || !update2.userId) return reject(req, reply, "telegram correlation invalid", updateId);
|
|
25432
|
+
const path = req.url.split("?")[0] ?? req.url;
|
|
25433
|
+
if (!isDestructiveTelegramRequest(req.method, path, req.body)) return;
|
|
25434
|
+
const confirmationId = header(req, "x-hanoman-telegram-confirmation");
|
|
25435
|
+
if (!confirmationId) return reject(req, reply, "telegram confirmation required", updateId);
|
|
25436
|
+
const consumed = await prisma.telegramConfirmation.updateMany({
|
|
25437
|
+
where: {
|
|
25438
|
+
id: confirmationId,
|
|
25439
|
+
state: "approved",
|
|
25440
|
+
updateId,
|
|
25441
|
+
chatId: update2.chatId,
|
|
25442
|
+
userId: update2.userId,
|
|
25443
|
+
method: req.method,
|
|
25444
|
+
path,
|
|
25445
|
+
expiresAt: { gt: /* @__PURE__ */ new Date() }
|
|
25446
|
+
},
|
|
25447
|
+
data: { state: "used", usedAt: /* @__PURE__ */ new Date() }
|
|
25448
|
+
});
|
|
25449
|
+
if (consumed.count !== 1) return reject(req, reply, "telegram confirmation invalid", updateId);
|
|
25450
|
+
await store2.audit({
|
|
25451
|
+
chatId: update2.chatId,
|
|
25452
|
+
userId: update2.userId,
|
|
25453
|
+
updateId,
|
|
25454
|
+
action: "confirmation-consume",
|
|
25455
|
+
outcome: "accepted",
|
|
25456
|
+
correlationId: `tg:${updateId}`,
|
|
25457
|
+
method: req.method,
|
|
25458
|
+
path
|
|
25459
|
+
}).catch(() => {
|
|
25460
|
+
});
|
|
25461
|
+
}
|
|
25462
|
+
async function auditTelegramGatewayResponse(req, reply) {
|
|
25463
|
+
const gatewayId = telegramGatewayAgentTokenId();
|
|
25464
|
+
if (!gatewayId || req.agent?.id !== gatewayId) return;
|
|
25465
|
+
const value = header(req, "x-hanoman-telegram-update");
|
|
25466
|
+
const updateId = value && /^\d+$/.test(value) ? Number(value) : null;
|
|
25467
|
+
const update2 = updateId === null ? null : await prisma.telegramUpdate.findUnique({ where: { updateId } });
|
|
25468
|
+
await store2.audit({
|
|
25469
|
+
chatId: update2?.chatId ?? null,
|
|
25470
|
+
userId: update2?.userId ?? null,
|
|
25471
|
+
updateId,
|
|
25472
|
+
action: "api-request",
|
|
25473
|
+
outcome: reply.statusCode < 400 ? "accepted" : "rejected",
|
|
25474
|
+
correlationId: updateId === null ? null : `tg:${updateId}`,
|
|
25475
|
+
method: req.method,
|
|
25476
|
+
path: req.url.split("?")[0] ?? req.url,
|
|
25477
|
+
statusCode: reply.statusCode
|
|
25478
|
+
}).catch(() => {
|
|
25479
|
+
});
|
|
25480
|
+
}
|
|
25481
|
+
|
|
25482
|
+
// src/app.ts
|
|
24031
25483
|
var PUBLIC = /* @__PURE__ */ new Set([
|
|
24032
25484
|
"GET /api/health",
|
|
24033
25485
|
"GET /api/auth/status",
|
|
@@ -24047,6 +25499,7 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
24047
25499
|
});
|
|
24048
25500
|
app2.register(websocket);
|
|
24049
25501
|
app2.addHook("onClose", async () => {
|
|
25502
|
+
await stopTelegramRuntime();
|
|
24050
25503
|
detachAll();
|
|
24051
25504
|
});
|
|
24052
25505
|
app2.register(async (api) => {
|
|
@@ -24080,6 +25533,8 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
24080
25533
|
return reply.code(401).send({ error: "unauthorized" });
|
|
24081
25534
|
});
|
|
24082
25535
|
}
|
|
25536
|
+
api.addHook("preHandler", guardTelegramGatewayRequest);
|
|
25537
|
+
api.addHook("onResponse", auditTelegramGatewayResponse);
|
|
24083
25538
|
await api.register(auth_default);
|
|
24084
25539
|
await api.register(health_default);
|
|
24085
25540
|
await api.register(projects_default);
|
|
@@ -24107,6 +25562,8 @@ function buildApp({ requireAuth = true } = {}) {
|
|
|
24107
25562
|
await api.register(codex);
|
|
24108
25563
|
await api.register(lead_default);
|
|
24109
25564
|
await api.register(custom_agents_default);
|
|
25565
|
+
await api.register(githubIssues);
|
|
25566
|
+
await api.register(telegramRoutes);
|
|
24110
25567
|
}, { prefix: "/api" });
|
|
24111
25568
|
if (process.env.NODE_ENV === "production") {
|
|
24112
25569
|
const dist = pickWebDir(dirname12(fileURLToPath4(import.meta.url)), process.env, existsSync11);
|
|
@@ -24224,13 +25681,14 @@ async function reconcile(deps) {
|
|
|
24224
25681
|
}
|
|
24225
25682
|
if (stage === "done") {
|
|
24226
25683
|
await recordCompletion(item.specId, spec.title, spec.projectId);
|
|
25684
|
+
const head = p?.cwd ? await recordHeadSha(item.specId, p.cwd, deps.headSha) : null;
|
|
24227
25685
|
const existing = await prisma.sessionResult.findFirst({ where: { specId: item.specId, newStage: "done" } });
|
|
24228
25686
|
if (!existing) {
|
|
24229
25687
|
await recordSessionResult({
|
|
24230
25688
|
projectId: spec.projectId,
|
|
24231
25689
|
specId: item.specId,
|
|
24232
25690
|
newStage: "done",
|
|
24233
|
-
commitSha:
|
|
25691
|
+
commitSha: head,
|
|
24234
25692
|
branch: `hanoman/${item.sessionId}`,
|
|
24235
25693
|
status: "done"
|
|
24236
25694
|
}).catch(() => {
|
|
@@ -24376,6 +25834,587 @@ function registerTriaseSource() {
|
|
|
24376
25834
|
|
|
24377
25835
|
// src/server.ts
|
|
24378
25836
|
init_pty();
|
|
25837
|
+
|
|
25838
|
+
// src/services/telegram/bootstrap.ts
|
|
25839
|
+
init_src2();
|
|
25840
|
+
init_db();
|
|
25841
|
+
import { mkdirSync as mkdirSync7 } from "node:fs";
|
|
25842
|
+
init_pty();
|
|
25843
|
+
|
|
25844
|
+
// src/services/telegram/client.ts
|
|
25845
|
+
var TelegramApiError = class extends Error {
|
|
25846
|
+
constructor(method, code, message) {
|
|
25847
|
+
super(`Telegram ${method} gagal${code ? ` (${code})` : ""}: ${message}`);
|
|
25848
|
+
this.method = method;
|
|
25849
|
+
this.code = code;
|
|
25850
|
+
this.name = "TelegramApiError";
|
|
25851
|
+
}
|
|
25852
|
+
};
|
|
25853
|
+
var TelegramApiClient = class {
|
|
25854
|
+
constructor(token, transport = (url2, init) => fetch(url2, init), baseUrl = "https://api.telegram.org") {
|
|
25855
|
+
this.token = token;
|
|
25856
|
+
this.transport = transport;
|
|
25857
|
+
this.baseUrl = baseUrl;
|
|
25858
|
+
}
|
|
25859
|
+
safe(message) {
|
|
25860
|
+
return message.split(this.token).join("[REDACTED]").replace(/https:\/\/api\.telegram\.org\/bot[^/\s]+/g, "https://api.telegram.org/bot[REDACTED]");
|
|
25861
|
+
}
|
|
25862
|
+
async call(method, body, signal) {
|
|
25863
|
+
let response;
|
|
25864
|
+
try {
|
|
25865
|
+
response = await this.transport(`${this.baseUrl}/bot${this.token}/${method}`, {
|
|
25866
|
+
method: "POST",
|
|
25867
|
+
headers: { "content-type": "application/json" },
|
|
25868
|
+
body: JSON.stringify(body),
|
|
25869
|
+
signal
|
|
25870
|
+
});
|
|
25871
|
+
} catch (error) {
|
|
25872
|
+
throw new TelegramApiError(method, void 0, this.safe(error.message || "network error"));
|
|
25873
|
+
}
|
|
25874
|
+
if (!response.ok) throw new TelegramApiError(method, response.status, `HTTP ${response.status}`);
|
|
25875
|
+
let envelope;
|
|
25876
|
+
try {
|
|
25877
|
+
envelope = await response.json();
|
|
25878
|
+
} catch {
|
|
25879
|
+
throw new TelegramApiError(method, response.status, "respons bukan JSON");
|
|
25880
|
+
}
|
|
25881
|
+
if (!envelope.ok || envelope.result === void 0) {
|
|
25882
|
+
throw new TelegramApiError(method, envelope.error_code, this.safe(envelope.description ?? "respons tidak valid"));
|
|
25883
|
+
}
|
|
25884
|
+
return envelope.result;
|
|
25885
|
+
}
|
|
25886
|
+
getMe() {
|
|
25887
|
+
return this.call("getMe", {});
|
|
25888
|
+
}
|
|
25889
|
+
async getUpdates(input) {
|
|
25890
|
+
if (!Number.isInteger(input.offset) || input.offset < 0) throw new Error("offset harus integer non-negatif");
|
|
25891
|
+
if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100) throw new Error("limit harus 1..100");
|
|
25892
|
+
if (!Number.isInteger(input.timeout) || input.timeout < 0 || input.timeout > 50) throw new Error("timeout harus 0..50");
|
|
25893
|
+
return this.call("getUpdates", {
|
|
25894
|
+
offset: input.offset,
|
|
25895
|
+
limit: input.limit,
|
|
25896
|
+
timeout: input.timeout,
|
|
25897
|
+
allowed_updates: ["message", "callback_query"]
|
|
25898
|
+
}, input.signal);
|
|
25899
|
+
}
|
|
25900
|
+
sendMessage(input) {
|
|
25901
|
+
return this.call("sendMessage", {
|
|
25902
|
+
chat_id: input.chatId,
|
|
25903
|
+
text: input.text,
|
|
25904
|
+
...input.replyMarkup ? { reply_markup: input.replyMarkup } : {}
|
|
25905
|
+
});
|
|
25906
|
+
}
|
|
25907
|
+
editMessageText(input) {
|
|
25908
|
+
return this.call("editMessageText", {
|
|
25909
|
+
chat_id: input.chatId,
|
|
25910
|
+
message_id: input.messageId,
|
|
25911
|
+
text: input.text
|
|
25912
|
+
});
|
|
25913
|
+
}
|
|
25914
|
+
answerCallbackQuery(input) {
|
|
25915
|
+
return this.call("answerCallbackQuery", {
|
|
25916
|
+
callback_query_id: input.callbackQueryId,
|
|
25917
|
+
...input.text ? { text: input.text } : {}
|
|
25918
|
+
});
|
|
25919
|
+
}
|
|
25920
|
+
};
|
|
25921
|
+
|
|
25922
|
+
// src/services/telegram/gateway.ts
|
|
25923
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
25924
|
+
|
|
25925
|
+
// src/services/telegram/protocol.ts
|
|
25926
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
25927
|
+
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
25928
|
+
function normalizeTelegramCommand(text) {
|
|
25929
|
+
const clean = text.trim();
|
|
25930
|
+
const match = clean.match(/^\/([a-z0-9_]+)(?:@[a-z0-9_]+)?(?:\s+([\s\S]*))?$/i);
|
|
25931
|
+
if (!match) return clean;
|
|
25932
|
+
const args = match[2]?.trim().replace(/\s+/g, " ");
|
|
25933
|
+
return `/${match[1].toLowerCase()}${args ? ` ${args}` : ""}`;
|
|
25934
|
+
}
|
|
25935
|
+
function parseConfirmationCallback(data) {
|
|
25936
|
+
if (data.length > 64) return null;
|
|
25937
|
+
const match = data.match(/^tgcf:([A-Za-z0-9_-]{1,40}):(approve|deny)$/);
|
|
25938
|
+
return match ? { token: match[1], action: match[2] } : null;
|
|
25939
|
+
}
|
|
25940
|
+
function parseTelegramUpdate(update2, allowlist, maxText = 12e3) {
|
|
25941
|
+
const root = record(update2);
|
|
25942
|
+
const updateId = root && Number.isInteger(root.update_id) && Number(root.update_id) >= 0 ? Number(root.update_id) : null;
|
|
25943
|
+
if (!root || updateId === null) return { ok: false, updateId: null, reason: "malformed" };
|
|
25944
|
+
const callback = record(root.callback_query);
|
|
25945
|
+
const message = callback ? record(callback.message) : record(root.message);
|
|
25946
|
+
const from = callback ? record(callback.from) : record(message?.from);
|
|
25947
|
+
const chat = record(message?.chat);
|
|
25948
|
+
const chatId = typeof chat?.id === "number" && Number.isSafeInteger(chat.id) ? String(chat.id) : void 0;
|
|
25949
|
+
const userId = typeof from?.id === "number" && Number.isSafeInteger(from.id) ? String(from.id) : void 0;
|
|
25950
|
+
if (!message || !from || !chat || !chatId || !userId) return { ok: false, updateId, reason: "malformed", chatId, userId };
|
|
25951
|
+
if (chat.type !== "private") return { ok: false, updateId, reason: "group", chatId, userId };
|
|
25952
|
+
if (from.is_bot === true) return { ok: false, updateId, reason: "sender-bot", chatId, userId };
|
|
25953
|
+
if (!allowlist.has(userId)) return { ok: false, updateId, reason: "not-allowed", chatId, userId };
|
|
25954
|
+
const messageId = typeof message.message_id === "number" && Number.isInteger(message.message_id) ? message.message_id : null;
|
|
25955
|
+
if (messageId === null) return { ok: false, updateId, reason: "malformed", chatId, userId };
|
|
25956
|
+
if (callback) {
|
|
25957
|
+
const callbackQueryId = typeof callback.id === "string" ? callback.id : "";
|
|
25958
|
+
const data = typeof callback.data === "string" ? callback.data : "";
|
|
25959
|
+
const parsed = parseConfirmationCallback(data);
|
|
25960
|
+
if (!callbackQueryId || !parsed) return { ok: false, updateId, reason: "invalid-callback", chatId, userId };
|
|
25961
|
+
return {
|
|
25962
|
+
ok: true,
|
|
25963
|
+
input: {
|
|
25964
|
+
updateId,
|
|
25965
|
+
chatId,
|
|
25966
|
+
userId,
|
|
25967
|
+
messageId,
|
|
25968
|
+
kind: "callback",
|
|
25969
|
+
text: data,
|
|
25970
|
+
callbackQueryId,
|
|
25971
|
+
callbackToken: parsed.token,
|
|
25972
|
+
callbackAction: parsed.action
|
|
25973
|
+
}
|
|
25974
|
+
};
|
|
25975
|
+
}
|
|
25976
|
+
if (typeof message.text !== "string" || !message.text.trim()) {
|
|
25977
|
+
return { ok: false, updateId, reason: "non-text", chatId, userId };
|
|
25978
|
+
}
|
|
25979
|
+
const text = normalizeTelegramCommand(message.text);
|
|
25980
|
+
if (text.length > maxText) return { ok: false, updateId, reason: "too-long", chatId, userId };
|
|
25981
|
+
return {
|
|
25982
|
+
ok: true,
|
|
25983
|
+
input: {
|
|
25984
|
+
updateId,
|
|
25985
|
+
chatId,
|
|
25986
|
+
userId,
|
|
25987
|
+
messageId,
|
|
25988
|
+
kind: text.startsWith("/") ? "command" : "text",
|
|
25989
|
+
text
|
|
25990
|
+
}
|
|
25991
|
+
};
|
|
25992
|
+
}
|
|
25993
|
+
function inboundDigest(input) {
|
|
25994
|
+
return createHash5("sha256").update(`${input.updateId}\0${input.chatId}\0${input.userId}\0${input.kind}\0${input.text}`, "utf8").digest("hex");
|
|
25995
|
+
}
|
|
25996
|
+
var ANSI = /[\u001b\u009b](?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
|
|
25997
|
+
var CONTROLS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
|
|
25998
|
+
var CREDENTIAL_PATTERNS = [
|
|
25999
|
+
/\b\d{6,}:[A-Za-z0-9_-]{20,}\b/g,
|
|
26000
|
+
/\bhnm_(?:agent_)?[A-Za-z0-9_-]{16,}\b/g,
|
|
26001
|
+
/\bsk-ant-[A-Za-z0-9_-]{16,}\b/g
|
|
26002
|
+
];
|
|
26003
|
+
function sanitizeTelegramOutput(text, exactSecrets = []) {
|
|
26004
|
+
let clean = text.replace(ANSI, "").replace(CONTROLS, "");
|
|
26005
|
+
for (const secret of [...exactSecrets].filter(Boolean).sort((a, b) => b.length - a.length)) {
|
|
26006
|
+
clean = clean.split(secret).join("[REDACTED]");
|
|
26007
|
+
}
|
|
26008
|
+
for (const pattern of CREDENTIAL_PATTERNS) clean = clean.replace(pattern, "[REDACTED]");
|
|
26009
|
+
return clean.trim();
|
|
26010
|
+
}
|
|
26011
|
+
function splitTelegramText(text, max = 4096, exactSecrets = []) {
|
|
26012
|
+
if (!Number.isInteger(max) || max < 1) throw new Error("max harus integer positif");
|
|
26013
|
+
const clean = sanitizeTelegramOutput(text, exactSecrets);
|
|
26014
|
+
if (!clean) return [];
|
|
26015
|
+
const chunks = [];
|
|
26016
|
+
let rest = clean;
|
|
26017
|
+
while (rest.length > max) {
|
|
26018
|
+
let cut = max;
|
|
26019
|
+
const before = rest.charCodeAt(cut - 1);
|
|
26020
|
+
const after = rest.charCodeAt(cut);
|
|
26021
|
+
if (before >= 55296 && before <= 56319 && after >= 56320 && after <= 57343) cut--;
|
|
26022
|
+
const window = rest.slice(0, cut);
|
|
26023
|
+
const newline2 = window.lastIndexOf("\n");
|
|
26024
|
+
const space = window.lastIndexOf(" ");
|
|
26025
|
+
const boundary = Math.max(newline2, space);
|
|
26026
|
+
if (boundary >= Math.floor(max / 2)) cut = boundary + 1;
|
|
26027
|
+
chunks.push(rest.slice(0, cut));
|
|
26028
|
+
rest = rest.slice(cut);
|
|
26029
|
+
}
|
|
26030
|
+
if (rest) chunks.push(rest);
|
|
26031
|
+
return chunks;
|
|
26032
|
+
}
|
|
26033
|
+
|
|
26034
|
+
// src/services/telegram/gateway.ts
|
|
26035
|
+
var digestRaw = (update2) => createHash6("sha256").update(JSON.stringify(update2)).digest("hex");
|
|
26036
|
+
var TelegramGateway = class {
|
|
26037
|
+
constructor(deps) {
|
|
26038
|
+
this.deps = deps;
|
|
26039
|
+
}
|
|
26040
|
+
abort = null;
|
|
26041
|
+
loopPromise = null;
|
|
26042
|
+
async processUpdates(updates) {
|
|
26043
|
+
for (const raw of updates) await this.processUpdate(raw);
|
|
26044
|
+
}
|
|
26045
|
+
async processUpdate(raw) {
|
|
26046
|
+
const parsed = parseTelegramUpdate(raw, this.deps.allowedUserIds);
|
|
26047
|
+
const root = typeof raw === "object" && raw !== null ? raw : {};
|
|
26048
|
+
const rawUpdateId = Number.isInteger(root.update_id) ? Number(root.update_id) : null;
|
|
26049
|
+
if (rawUpdateId === null || rawUpdateId < 0) return;
|
|
26050
|
+
const meta = parsed.ok ? {
|
|
26051
|
+
updateId: parsed.input.updateId,
|
|
26052
|
+
chatId: parsed.input.chatId,
|
|
26053
|
+
userId: parsed.input.userId,
|
|
26054
|
+
kind: parsed.input.kind,
|
|
26055
|
+
digest: inboundDigest(parsed.input)
|
|
26056
|
+
} : {
|
|
26057
|
+
updateId: rawUpdateId,
|
|
26058
|
+
chatId: parsed.chatId ?? null,
|
|
26059
|
+
userId: parsed.userId ?? null,
|
|
26060
|
+
kind: "rejected",
|
|
26061
|
+
digest: digestRaw(raw)
|
|
26062
|
+
};
|
|
26063
|
+
if (!await this.deps.store.recordUpdate(meta)) return;
|
|
26064
|
+
if (!parsed.ok) {
|
|
26065
|
+
await this.deps.store.rejectUpdate(rawUpdateId, parsed.reason);
|
|
26066
|
+
await this.deps.store.audit({
|
|
26067
|
+
chatId: parsed.chatId ?? null,
|
|
26068
|
+
userId: parsed.userId ?? null,
|
|
26069
|
+
updateId: rawUpdateId,
|
|
26070
|
+
action: "inbound",
|
|
26071
|
+
outcome: `rejected:${parsed.reason}`,
|
|
26072
|
+
correlationId: `tg:${rawUpdateId}`
|
|
26073
|
+
});
|
|
26074
|
+
return;
|
|
26075
|
+
}
|
|
26076
|
+
const input = parsed.input;
|
|
26077
|
+
if (await this.deps.store.rateLimitExceeded(
|
|
26078
|
+
input.userId,
|
|
26079
|
+
new Date(Date.now() - this.deps.rateLimit.windowMs),
|
|
26080
|
+
this.deps.rateLimit.limit
|
|
26081
|
+
)) {
|
|
26082
|
+
await this.deps.store.rejectUpdate(input.updateId, "rate-limit");
|
|
26083
|
+
await this.deps.store.audit({
|
|
26084
|
+
chatId: input.chatId,
|
|
26085
|
+
userId: input.userId,
|
|
26086
|
+
updateId: input.updateId,
|
|
26087
|
+
action: "inbound",
|
|
26088
|
+
outcome: "rejected:rate-limit",
|
|
26089
|
+
correlationId: `tg:${input.updateId}`
|
|
26090
|
+
});
|
|
26091
|
+
return;
|
|
26092
|
+
}
|
|
26093
|
+
if (!await this.deps.store.claimUpdate(input.updateId)) return;
|
|
26094
|
+
try {
|
|
26095
|
+
if (input.kind === "callback" && input.callbackToken && input.callbackAction && input.callbackQueryId) {
|
|
26096
|
+
const outcome = await this.deps.store.resolveConfirmation({
|
|
26097
|
+
callbackToken: input.callbackToken,
|
|
26098
|
+
chatId: input.chatId,
|
|
26099
|
+
userId: input.userId,
|
|
26100
|
+
action: input.callbackAction
|
|
26101
|
+
});
|
|
26102
|
+
const answer = outcome === "approved" ? "Disetujui" : outcome === "denied" ? "Dibatalkan" : outcome === "expired" ? "Konfirmasi kedaluwarsa" : "Konfirmasi tidak valid";
|
|
26103
|
+
await this.deps.client.answerCallbackQuery({ callbackQueryId: input.callbackQueryId, text: answer });
|
|
26104
|
+
if (outcome !== "approved" && outcome !== "denied") {
|
|
26105
|
+
await this.deps.store.markDispatched(input.updateId);
|
|
26106
|
+
return;
|
|
26107
|
+
}
|
|
26108
|
+
input.text = `[confirmation ${outcome}] ${input.callbackToken}`;
|
|
26109
|
+
}
|
|
26110
|
+
const target2 = await this.deps.dispatcher.dispatch(input);
|
|
26111
|
+
await this.deps.store.markDispatched(input.updateId);
|
|
26112
|
+
await this.deps.store.audit({
|
|
26113
|
+
chatId: input.chatId,
|
|
26114
|
+
userId: input.userId,
|
|
26115
|
+
updateId: input.updateId,
|
|
26116
|
+
action: "dispatch",
|
|
26117
|
+
outcome: target2.created ? "session-created" : "session-reused",
|
|
26118
|
+
correlationId: `tg:${input.updateId}`
|
|
26119
|
+
});
|
|
26120
|
+
updateTelegramRuntimeStatus({ lastUpdateAt: (/* @__PURE__ */ new Date()).toISOString(), lastError: null });
|
|
26121
|
+
} catch (error) {
|
|
26122
|
+
await this.deps.store.markUpdateUncertain(input.updateId, "dispatch-failed");
|
|
26123
|
+
await this.deps.store.audit({
|
|
26124
|
+
chatId: input.chatId,
|
|
26125
|
+
userId: input.userId,
|
|
26126
|
+
updateId: input.updateId,
|
|
26127
|
+
action: "dispatch",
|
|
26128
|
+
outcome: "uncertain",
|
|
26129
|
+
correlationId: `tg:${input.updateId}`
|
|
26130
|
+
});
|
|
26131
|
+
throw error;
|
|
26132
|
+
}
|
|
26133
|
+
}
|
|
26134
|
+
async flushOutbox() {
|
|
26135
|
+
for (; ; ) {
|
|
26136
|
+
const row = await this.deps.store.claimNextOutbox();
|
|
26137
|
+
if (!row) return;
|
|
26138
|
+
try {
|
|
26139
|
+
const text = sanitizeTelegramOutput(row.text, this.deps.exactSecrets);
|
|
26140
|
+
const chunks = splitTelegramText(text);
|
|
26141
|
+
if (!chunks.length) {
|
|
26142
|
+
await this.deps.store.markOutboxFailed(row.id, "empty-after-sanitize");
|
|
26143
|
+
continue;
|
|
26144
|
+
}
|
|
26145
|
+
const confirmation = row.confirmationId ? await this.deps.store.confirmation(row.confirmationId) : null;
|
|
26146
|
+
let messageId = 0;
|
|
26147
|
+
for (let index = 0; index < chunks.length; index++) {
|
|
26148
|
+
const isLast = index === chunks.length - 1;
|
|
26149
|
+
const replyMarkup = isLast && confirmation ? {
|
|
26150
|
+
inline_keyboard: [[
|
|
26151
|
+
{ text: "Lanjutkan", callback_data: `tgcf:${confirmation.callbackToken}:approve` },
|
|
26152
|
+
{ text: "Batalkan", callback_data: `tgcf:${confirmation.callbackToken}:deny` }
|
|
26153
|
+
]]
|
|
26154
|
+
} : void 0;
|
|
26155
|
+
const message = await this.deps.client.sendMessage({ chatId: row.chatId, text: chunks[index], replyMarkup });
|
|
26156
|
+
messageId = message.message_id;
|
|
26157
|
+
}
|
|
26158
|
+
await this.deps.store.markOutboxSent(row.id, messageId);
|
|
26159
|
+
} catch (error) {
|
|
26160
|
+
const message = sanitizeTelegramOutput(error.message, this.deps.exactSecrets).slice(0, 500);
|
|
26161
|
+
if (error instanceof TelegramApiError && error.code !== void 0) {
|
|
26162
|
+
await this.deps.store.markOutboxFailed(row.id, message);
|
|
26163
|
+
} else {
|
|
26164
|
+
await this.deps.store.markOutboxUncertain(row.id, message || "network-outcome-unknown");
|
|
26165
|
+
}
|
|
26166
|
+
}
|
|
26167
|
+
}
|
|
26168
|
+
}
|
|
26169
|
+
async start() {
|
|
26170
|
+
if (this.loopPromise) return;
|
|
26171
|
+
await this.deps.store.recoverUncertainClaims();
|
|
26172
|
+
await this.deps.store.recoverUncertainOutbox();
|
|
26173
|
+
this.abort = new AbortController();
|
|
26174
|
+
updateTelegramRuntimeStatus({ running: true, readiness: "running", lastError: null });
|
|
26175
|
+
this.loopPromise = this.loop(this.abort.signal).finally(() => {
|
|
26176
|
+
this.loopPromise = null;
|
|
26177
|
+
this.abort = null;
|
|
26178
|
+
updateTelegramRuntimeStatus({ running: false });
|
|
26179
|
+
});
|
|
26180
|
+
}
|
|
26181
|
+
async loop(signal) {
|
|
26182
|
+
while (!signal.aborted) {
|
|
26183
|
+
try {
|
|
26184
|
+
const updates = await this.deps.client.getUpdates({
|
|
26185
|
+
offset: await this.deps.store.offset(),
|
|
26186
|
+
limit: 100,
|
|
26187
|
+
timeout: this.deps.pollTimeout ?? 25,
|
|
26188
|
+
signal
|
|
26189
|
+
});
|
|
26190
|
+
await this.processUpdates(updates);
|
|
26191
|
+
await this.flushOutbox();
|
|
26192
|
+
} catch (error) {
|
|
26193
|
+
if (signal.aborted) return;
|
|
26194
|
+
const message = sanitizeTelegramOutput(error.message, this.deps.exactSecrets).slice(0, 500);
|
|
26195
|
+
updateTelegramRuntimeStatus({ running: false, readiness: "error", lastError: message });
|
|
26196
|
+
if (error instanceof TelegramApiError && error.code === 409) return;
|
|
26197
|
+
await new Promise((resolve14) => setTimeout(resolve14, 1e3));
|
|
26198
|
+
}
|
|
26199
|
+
}
|
|
26200
|
+
}
|
|
26201
|
+
async stop() {
|
|
26202
|
+
this.abort?.abort();
|
|
26203
|
+
await this.loopPromise;
|
|
26204
|
+
}
|
|
26205
|
+
};
|
|
26206
|
+
|
|
26207
|
+
// src/services/telegram/session.ts
|
|
26208
|
+
init_src2();
|
|
26209
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
26210
|
+
var chatHash = (chatId) => createHash7("sha256").update(chatId).digest("hex").slice(0, 16);
|
|
26211
|
+
var telegramOperatorSessionId = (chatId) => `telegram-${chatHash(chatId)}`;
|
|
26212
|
+
var formatTelegramTurn = (input) => `[Telegram update ${input.updateId} \xB7 chat ${input.chatId} \xB7 kind ${input.kind}]
|
|
26213
|
+
${input.text}`;
|
|
26214
|
+
var TelegramSessionCoordinator = class {
|
|
26215
|
+
constructor(deps) {
|
|
26216
|
+
this.deps = deps;
|
|
26217
|
+
}
|
|
26218
|
+
async dispatch(input) {
|
|
26219
|
+
let context = await this.deps.store.chatContext(input.chatId);
|
|
26220
|
+
if (!context) {
|
|
26221
|
+
const defaults = await this.deps.defaults();
|
|
26222
|
+
await this.deps.store.ensureChat({
|
|
26223
|
+
chatId: input.chatId,
|
|
26224
|
+
userId: input.userId,
|
|
26225
|
+
agent: defaults.agent,
|
|
26226
|
+
model: defaults.model,
|
|
26227
|
+
effort: defaults.effort
|
|
26228
|
+
});
|
|
26229
|
+
context = await this.deps.store.chatContext(input.chatId);
|
|
26230
|
+
}
|
|
26231
|
+
if (!context) throw new Error("gagal membuat binding chat Telegram");
|
|
26232
|
+
const sessionId2 = telegramOperatorSessionId(input.chatId);
|
|
26233
|
+
const live = this.deps.port.getSession(sessionId2);
|
|
26234
|
+
if (live && !live.exited) {
|
|
26235
|
+
if (!await this.deps.port.sendToPane(sessionId2, formatTelegramTurn(input))) {
|
|
26236
|
+
throw new Error("pane operator tidak menerima steer");
|
|
26237
|
+
}
|
|
26238
|
+
if (context.sessionId !== sessionId2) await this.deps.store.bindSession(input.chatId, sessionId2);
|
|
26239
|
+
return { sessionId: sessionId2, created: false };
|
|
26240
|
+
}
|
|
26241
|
+
const hash2 = chatHash(input.chatId);
|
|
26242
|
+
const projectId = `telegram:${hash2}`;
|
|
26243
|
+
const cwd = `${this.deps.home.replace(/\/$/, "")}/telegram/${hash2}`;
|
|
26244
|
+
this.deps.ensureDir(cwd);
|
|
26245
|
+
if (context.agent === "codex") this.deps.ensureCodexTrust(cwd);
|
|
26246
|
+
const personality = await this.deps.personality(context.personalityAgentId, context.activeProjectId);
|
|
26247
|
+
const prompt = buildTelegramOperatorPrompt({
|
|
26248
|
+
update: input,
|
|
26249
|
+
personality,
|
|
26250
|
+
summary: context.summary,
|
|
26251
|
+
memories: context.memories
|
|
26252
|
+
});
|
|
26253
|
+
const born = this.deps.port.createSession(projectId, cwd, {
|
|
26254
|
+
id: sessionId2,
|
|
26255
|
+
prompt,
|
|
26256
|
+
agent: context.agent,
|
|
26257
|
+
model: context.model,
|
|
26258
|
+
effort: context.effort,
|
|
26259
|
+
env: {
|
|
26260
|
+
HANOMAN_API_BASE: this.deps.apiBase,
|
|
26261
|
+
HANOMAN_TELEGRAM_AGENT_TOKEN: this.deps.agentToken,
|
|
26262
|
+
HANOMAN_TELEGRAM_CHAT_ID: input.chatId
|
|
26263
|
+
}
|
|
26264
|
+
});
|
|
26265
|
+
if (born.id !== sessionId2 || born.exited) throw new Error("pane operator gagal lahir");
|
|
26266
|
+
await this.deps.store.bindSession(input.chatId, sessionId2);
|
|
26267
|
+
return { sessionId: sessionId2, created: true };
|
|
26268
|
+
}
|
|
26269
|
+
};
|
|
26270
|
+
|
|
26271
|
+
// src/services/telegram/bootstrap.ts
|
|
26272
|
+
var TELEGRAM_REQUIRED_CAPABILITIES = [
|
|
26273
|
+
"projects:read",
|
|
26274
|
+
"projects:write",
|
|
26275
|
+
"backlog:read",
|
|
26276
|
+
"backlog:write",
|
|
26277
|
+
"sessions:read",
|
|
26278
|
+
"sessions:write",
|
|
26279
|
+
"docs:read",
|
|
26280
|
+
"docs:write",
|
|
26281
|
+
"ide:read",
|
|
26282
|
+
"ide:write",
|
|
26283
|
+
"vps:read",
|
|
26284
|
+
"vps:write",
|
|
26285
|
+
"settings:read",
|
|
26286
|
+
"settings:write",
|
|
26287
|
+
"support:read",
|
|
26288
|
+
"support:write",
|
|
26289
|
+
"notifications:read",
|
|
26290
|
+
"notifications:write",
|
|
26291
|
+
"lead:read",
|
|
26292
|
+
"lead:write",
|
|
26293
|
+
"agents:read",
|
|
26294
|
+
"telegram:read",
|
|
26295
|
+
"telegram:write"
|
|
26296
|
+
];
|
|
26297
|
+
function parseTelegramAllowedUserIds(raw) {
|
|
26298
|
+
const ids = raw.split(/[\s,]+/).filter(Boolean);
|
|
26299
|
+
if (!ids.length) throw new Error("Telegram allowlist wajib berisi numeric user id");
|
|
26300
|
+
if (ids.some((id) => !/^\d+$/.test(id))) throw new Error("Telegram allowlist hanya menerima numeric user id");
|
|
26301
|
+
return new Set(ids);
|
|
26302
|
+
}
|
|
26303
|
+
var configuredFrom = (env) => Boolean(env.HANOMAN_TELEGRAM_BOT_TOKEN?.trim() && env.HANOMAN_TELEGRAM_ALLOWED_USER_IDS?.trim() && env.HANOMAN_TELEGRAM_AGENT_TOKEN?.trim());
|
|
26304
|
+
async function productionFactory(input) {
|
|
26305
|
+
const client = new TelegramApiClient(input.botToken);
|
|
26306
|
+
const me = await client.getMe();
|
|
26307
|
+
const store3 = new TelegramStore(prisma);
|
|
26308
|
+
const coordinator = new TelegramSessionCoordinator({
|
|
26309
|
+
store: store3,
|
|
26310
|
+
port: { getSession, createSession, sendToPane },
|
|
26311
|
+
defaults: sessionAgentDefaults,
|
|
26312
|
+
personality: async (id, projectId) => {
|
|
26313
|
+
if (!id) return null;
|
|
26314
|
+
const row = await prisma.customAgent.findUnique({ where: { id } });
|
|
26315
|
+
if (!row?.enabled || row.projectId !== null && row.projectId !== projectId) return null;
|
|
26316
|
+
return { name: row.name, description: row.description, instructions: row.instructions };
|
|
26317
|
+
},
|
|
26318
|
+
ensureCodexTrust,
|
|
26319
|
+
home: resolveHome(),
|
|
26320
|
+
apiBase: input.apiBase,
|
|
26321
|
+
agentToken: input.agentToken,
|
|
26322
|
+
ensureDir: (path) => mkdirSync7(path, { recursive: true })
|
|
26323
|
+
});
|
|
26324
|
+
const gateway = new TelegramGateway({
|
|
26325
|
+
client,
|
|
26326
|
+
store: store3,
|
|
26327
|
+
dispatcher: coordinator,
|
|
26328
|
+
allowedUserIds: input.allowedUserIds,
|
|
26329
|
+
rateLimit: { limit: 20, windowMs: 6e4 },
|
|
26330
|
+
exactSecrets: [input.botToken, input.agentToken],
|
|
26331
|
+
progress: input.progress
|
|
26332
|
+
});
|
|
26333
|
+
return { gateway, botUsername: me.username ?? null };
|
|
26334
|
+
}
|
|
26335
|
+
async function installTelegramGateway(_app, options2) {
|
|
26336
|
+
const env = options2.env ?? process.env;
|
|
26337
|
+
const setting = await (options2.getSetting ?? getSetting)();
|
|
26338
|
+
const configured = configuredFrom(env);
|
|
26339
|
+
const base = {
|
|
26340
|
+
configured,
|
|
26341
|
+
enabled: setting.telegram.enabled,
|
|
26342
|
+
running: false,
|
|
26343
|
+
botUsername: null,
|
|
26344
|
+
allowlistCount: 0,
|
|
26345
|
+
agentTokenConfigured: Boolean(env.HANOMAN_TELEGRAM_AGENT_TOKEN?.trim()),
|
|
26346
|
+
missingCapabilities: [],
|
|
26347
|
+
lastUpdateAt: null,
|
|
26348
|
+
lastError: null
|
|
26349
|
+
};
|
|
26350
|
+
if (!setting.telegram.enabled) {
|
|
26351
|
+
setTelegramRuntime({ status: { ...base, readiness: "disabled" } });
|
|
26352
|
+
return;
|
|
26353
|
+
}
|
|
26354
|
+
if (!configured) {
|
|
26355
|
+
setTelegramRuntime({ status: { ...base, readiness: "misconfigured", lastError: "environment Telegram belum lengkap" } });
|
|
26356
|
+
return;
|
|
26357
|
+
}
|
|
26358
|
+
let allowedUserIds;
|
|
26359
|
+
try {
|
|
26360
|
+
allowedUserIds = parseTelegramAllowedUserIds(env.HANOMAN_TELEGRAM_ALLOWED_USER_IDS);
|
|
26361
|
+
} catch (error) {
|
|
26362
|
+
setTelegramRuntime({ status: { ...base, readiness: "misconfigured", lastError: error.message } });
|
|
26363
|
+
return;
|
|
26364
|
+
}
|
|
26365
|
+
const verify = options2.verifyAgentToken ?? verifyAgentToken;
|
|
26366
|
+
const agent = setting.agentAccessEnabled ? await verify(env.HANOMAN_TELEGRAM_AGENT_TOKEN) : null;
|
|
26367
|
+
const missing = agent ? TELEGRAM_REQUIRED_CAPABILITIES.filter((capability) => !agent.capabilities.includes(capability)) : [...TELEGRAM_REQUIRED_CAPABILITIES];
|
|
26368
|
+
if (!agent || missing.length) {
|
|
26369
|
+
setTelegramRuntime({
|
|
26370
|
+
agentTokenId: agent?.id ?? null,
|
|
26371
|
+
status: {
|
|
26372
|
+
...base,
|
|
26373
|
+
allowlistCount: allowedUserIds.size,
|
|
26374
|
+
missingCapabilities: missing,
|
|
26375
|
+
readiness: "misconfigured",
|
|
26376
|
+
lastError: agent ? "capability AgentToken Telegram belum lengkap" : "AgentToken Telegram tidak valid atau akses agent mati"
|
|
26377
|
+
}
|
|
26378
|
+
});
|
|
26379
|
+
return;
|
|
26380
|
+
}
|
|
26381
|
+
try {
|
|
26382
|
+
const built = await (options2.factory ?? productionFactory)({
|
|
26383
|
+
apiBase: options2.apiBase,
|
|
26384
|
+
botToken: env.HANOMAN_TELEGRAM_BOT_TOKEN,
|
|
26385
|
+
agentToken: env.HANOMAN_TELEGRAM_AGENT_TOKEN,
|
|
26386
|
+
allowedUserIds,
|
|
26387
|
+
progress: setting.telegram.progress
|
|
26388
|
+
});
|
|
26389
|
+
await built.gateway.start();
|
|
26390
|
+
setTelegramRuntime({
|
|
26391
|
+
agentTokenId: agent.id,
|
|
26392
|
+
status: {
|
|
26393
|
+
...base,
|
|
26394
|
+
configured: true,
|
|
26395
|
+
enabled: true,
|
|
26396
|
+
running: true,
|
|
26397
|
+
readiness: "running",
|
|
26398
|
+
botUsername: built.botUsername,
|
|
26399
|
+
allowlistCount: allowedUserIds.size,
|
|
26400
|
+
missingCapabilities: []
|
|
26401
|
+
}
|
|
26402
|
+
});
|
|
26403
|
+
registerTelegramRuntimeStop(() => built.gateway.stop());
|
|
26404
|
+
} catch {
|
|
26405
|
+
setTelegramRuntime({
|
|
26406
|
+
agentTokenId: agent.id,
|
|
26407
|
+
status: {
|
|
26408
|
+
...base,
|
|
26409
|
+
allowlistCount: allowedUserIds.size,
|
|
26410
|
+
readiness: "error",
|
|
26411
|
+
lastError: "gagal memverifikasi atau memulai Telegram Bot API"
|
|
26412
|
+
}
|
|
26413
|
+
});
|
|
26414
|
+
}
|
|
26415
|
+
}
|
|
26416
|
+
|
|
26417
|
+
// src/server.ts
|
|
24379
26418
|
var app = buildApp();
|
|
24380
26419
|
var port = Number(process.env.PORT ?? 8787);
|
|
24381
26420
|
var host = process.env.HOST ?? "127.0.0.1";
|
|
@@ -24404,6 +26443,8 @@ app.listen({ port, host }).then(async () => {
|
|
|
24404
26443
|
} catch (e) {
|
|
24405
26444
|
console.error("rekonsiliasi riwayat sesi dilewati \u2014 tmux tak terbaca:", e);
|
|
24406
26445
|
}
|
|
26446
|
+
const boundPort = app.server.address().port;
|
|
26447
|
+
await installTelegramGateway(app, { apiBase: `http://127.0.0.1:${boundPort}` });
|
|
24407
26448
|
startVpsMonitor();
|
|
24408
26449
|
registerBacklogSource();
|
|
24409
26450
|
registerTriaseSource();
|