@jiraacp/cli 2026.405.4

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.
Files changed (113) hide show
  1. package/README.md +283 -0
  2. package/dist/abort-GQE4OI5S.js +103 -0
  3. package/dist/abort-GQE4OI5S.js.map +1 -0
  4. package/dist/abort-VMRQOADY.js +96 -0
  5. package/dist/abort-VMRQOADY.js.map +1 -0
  6. package/dist/bot-WOTETAJY.js +13 -0
  7. package/dist/bot-WOTETAJY.js.map +1 -0
  8. package/dist/cancel-clarification-4G5S2HJZ.js +64 -0
  9. package/dist/cancel-clarification-4G5S2HJZ.js.map +1 -0
  10. package/dist/chunk-3U373M37.js +67 -0
  11. package/dist/chunk-3U373M37.js.map +1 -0
  12. package/dist/chunk-3YHD4SIN.js +97 -0
  13. package/dist/chunk-3YHD4SIN.js.map +1 -0
  14. package/dist/chunk-6IY6CRUJ.js +690 -0
  15. package/dist/chunk-6IY6CRUJ.js.map +1 -0
  16. package/dist/chunk-B6OA3XJK.js +1167 -0
  17. package/dist/chunk-B6OA3XJK.js.map +1 -0
  18. package/dist/chunk-BM4R6NST.js +191 -0
  19. package/dist/chunk-BM4R6NST.js.map +1 -0
  20. package/dist/chunk-FLPIU2QO.js +77 -0
  21. package/dist/chunk-FLPIU2QO.js.map +1 -0
  22. package/dist/chunk-H7YXX4UA.js +86 -0
  23. package/dist/chunk-H7YXX4UA.js.map +1 -0
  24. package/dist/chunk-IT74N3UH.js +19 -0
  25. package/dist/chunk-IT74N3UH.js.map +1 -0
  26. package/dist/chunk-JOT4UVSO.js +186 -0
  27. package/dist/chunk-JOT4UVSO.js.map +1 -0
  28. package/dist/chunk-KSJKCLEJ.js +222 -0
  29. package/dist/chunk-KSJKCLEJ.js.map +1 -0
  30. package/dist/chunk-LIEW4ULF.js +139 -0
  31. package/dist/chunk-LIEW4ULF.js.map +1 -0
  32. package/dist/chunk-M4V3YOCY.js +82 -0
  33. package/dist/chunk-M4V3YOCY.js.map +1 -0
  34. package/dist/chunk-MMWQHH25.js +207 -0
  35. package/dist/chunk-MMWQHH25.js.map +1 -0
  36. package/dist/chunk-OJ4CNF73.js +78 -0
  37. package/dist/chunk-OJ4CNF73.js.map +1 -0
  38. package/dist/chunk-PFJAC3RO.js +137 -0
  39. package/dist/chunk-PFJAC3RO.js.map +1 -0
  40. package/dist/chunk-PVKVCUNR.js +159 -0
  41. package/dist/chunk-PVKVCUNR.js.map +1 -0
  42. package/dist/chunk-RXT4WSIY.js +35 -0
  43. package/dist/chunk-RXT4WSIY.js.map +1 -0
  44. package/dist/chunk-RZK74PDF.js +34 -0
  45. package/dist/chunk-RZK74PDF.js.map +1 -0
  46. package/dist/chunk-UDTWVKRX.js +68 -0
  47. package/dist/chunk-UDTWVKRX.js.map +1 -0
  48. package/dist/chunk-VCEONSWJ.js +307 -0
  49. package/dist/chunk-VCEONSWJ.js.map +1 -0
  50. package/dist/chunk-VWBCDZWQ.js +119 -0
  51. package/dist/chunk-VWBCDZWQ.js.map +1 -0
  52. package/dist/chunk-WEJCTFQB.js +228 -0
  53. package/dist/chunk-WEJCTFQB.js.map +1 -0
  54. package/dist/chunk-YJK7IRPI.js +223 -0
  55. package/dist/chunk-YJK7IRPI.js.map +1 -0
  56. package/dist/claude-md-HQ6L4CRP.js +8 -0
  57. package/dist/claude-md-HQ6L4CRP.js.map +1 -0
  58. package/dist/cli.js +276 -0
  59. package/dist/cli.js.map +1 -0
  60. package/dist/commands-RG45VBTZ.js +407 -0
  61. package/dist/commands-RG45VBTZ.js.map +1 -0
  62. package/dist/commands-WYVRVE5Z.js +400 -0
  63. package/dist/commands-WYVRVE5Z.js.map +1 -0
  64. package/dist/config-edit-G7O56HXO.js +50 -0
  65. package/dist/config-edit-G7O56HXO.js.map +1 -0
  66. package/dist/config-set-QN3JRNZL.js +63 -0
  67. package/dist/config-set-QN3JRNZL.js.map +1 -0
  68. package/dist/daemon-CGBV55JK.js +104 -0
  69. package/dist/daemon-CGBV55JK.js.map +1 -0
  70. package/dist/dashboard-YVFJ5DXR.js +143 -0
  71. package/dist/dashboard-YVFJ5DXR.js.map +1 -0
  72. package/dist/doctor-BPTLVLTD.js +98 -0
  73. package/dist/doctor-BPTLVLTD.js.map +1 -0
  74. package/dist/human-loop-RBTA2TYK.js +16 -0
  75. package/dist/human-loop-RBTA2TYK.js.map +1 -0
  76. package/dist/human-loop-XGWXUNCS.js +18 -0
  77. package/dist/human-loop-XGWXUNCS.js.map +1 -0
  78. package/dist/index.d.ts +583 -0
  79. package/dist/index.js +28 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/loader-DGW7HCJ5.js +21 -0
  82. package/dist/loader-DGW7HCJ5.js.map +1 -0
  83. package/dist/logs-JUVQWN6C.js +93 -0
  84. package/dist/logs-JUVQWN6C.js.map +1 -0
  85. package/dist/mcp.js +132 -0
  86. package/dist/mcp.js.map +1 -0
  87. package/dist/orchestrator-3MGXX3QW.js +22 -0
  88. package/dist/orchestrator-3MGXX3QW.js.map +1 -0
  89. package/dist/orchestrator-BVUKN5N3.js +13 -0
  90. package/dist/orchestrator-BVUKN5N3.js.map +1 -0
  91. package/dist/pause-FLDZ3OD6.js +62 -0
  92. package/dist/pause-FLDZ3OD6.js.map +1 -0
  93. package/dist/projects-QMIGNW7U.js +129 -0
  94. package/dist/projects-QMIGNW7U.js.map +1 -0
  95. package/dist/replay-M4JEG4Z4.js +151 -0
  96. package/dist/replay-M4JEG4Z4.js.map +1 -0
  97. package/dist/schedule-CDHD77VZ.js +17 -0
  98. package/dist/schedule-CDHD77VZ.js.map +1 -0
  99. package/dist/serve-XI7JTIPZ.js +231 -0
  100. package/dist/serve-XI7JTIPZ.js.map +1 -0
  101. package/dist/sprint-KZZWVNK6.js +200 -0
  102. package/dist/sprint-KZZWVNK6.js.map +1 -0
  103. package/dist/status-I6GU2LWE.js +48 -0
  104. package/dist/status-I6GU2LWE.js.map +1 -0
  105. package/dist/topic-manager-4AMEPMFI.js +12 -0
  106. package/dist/topic-manager-4AMEPMFI.js.map +1 -0
  107. package/dist/triage-WNHGPVZQ.js +251 -0
  108. package/dist/triage-WNHGPVZQ.js.map +1 -0
  109. package/dist/usage-AWWBI37F.js +155 -0
  110. package/dist/usage-AWWBI37F.js.map +1 -0
  111. package/dist/wizard-CYEJJLNF.js +190 -0
  112. package/dist/wizard-CYEJJLNF.js.map +1 -0
  113. package/package.json +56 -0
package/README.md ADDED
@@ -0,0 +1,283 @@
1
+ # jira-acp
2
+
3
+ > AI-powered Jira pipeline CLI: Ticket → Code → GitHub → Deploy → Notify
4
+
5
+ [![npm version](https://img.shields.io/npm/v/jira-acp.svg)](https://www.npmjs.com/package/jira-acp)
6
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ `jiraACP` automates the full ticket lifecycle for small dev teams. Pick up a Jira ticket in the morning — by the time you check Telegram, it's been analyzed, coded, reviewed, and deployed. Ambiguous tickets send a clarification message instead of silently failing.
10
+
11
+ ---
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install -g @jiraacp/cli
17
+ ```
18
+
19
+ **Requirements:** Node.js >= 20, [Claude Code](https://claude.ai/code) installed globally.
20
+
21
+ ---
22
+
23
+ ## Quick Start
24
+
25
+ ```bash
26
+ # 1. Configure a project (interactive wizard)
27
+ jiraACP init
28
+
29
+ # 2. Start the background server
30
+ jiraACP start
31
+
32
+ # 3. Run the pipeline for a ticket
33
+ jiraACP run PROJ-123
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Two Binaries
39
+
40
+ | Binary | Purpose |
41
+ |--------|---------|
42
+ | `jiraACP` | Pipeline CLI — all commands |
43
+ | `jiraACP-mcp` | Jira MCP server — auto-configured for Claude Code agents |
44
+
45
+ `jiraACP init` writes `.mcp.json` into your workspace so Claude Code agents have Jira tools available automatically.
46
+
47
+ ---
48
+
49
+ ## How It Works
50
+
51
+ Each **project** binds one Jira instance + one GitHub repo + one Claude Code workspace + one Telegram chat into a single orchestrated unit.
52
+
53
+ ### 9-Stage Pipeline
54
+
55
+ | Stage | What happens | Model |
56
+ |-------|-------------|-------|
57
+ | 1. Fetch | Pull assigned Jira tickets | Haiku |
58
+ | 2. Analyze | Clarity scoring — criteria, design, dependencies | Haiku→Sonnet |
59
+ | 3. Clarify | Telegram prompt if ambiguous, await human reply | Haiku |
60
+ | 4. Code | Claude Code implements the ticket in your workspace | Sonnet/Opus |
61
+ | 5. Git | Create branch, commit, push, open PR | Haiku |
62
+ | 6. Review | Two-agent PR review, auto-merge if clean | Sonnet×2 |
63
+ | 7. Deploy | Run your deploy script | Haiku |
64
+ | 8. Test | Playwright agent tests on dev server | Sonnet |
65
+ | 9. Notify | Jira: transition + comment. Telegram: done | Haiku |
66
+
67
+ Stage 4 auto-upgrades to Opus for auth, payments, DB schema changes, or cross-module refactoring.
68
+
69
+ ---
70
+
71
+ ## CLI Reference
72
+
73
+ ### Server (Daemon)
74
+
75
+ ```bash
76
+ jiraACP start [--port 3100] # Start background server
77
+ jiraACP stop # Stop background server
78
+ jiraACP restart # Restart background server
79
+ jiraACP status # Check if server is running
80
+ jiraACP logs [-f] [-n 100] # Tail server logs
81
+ ```
82
+
83
+ ### Setup
84
+
85
+ ```bash
86
+ jiraACP init [--dir <path>] # Interactive setup wizard
87
+ jiraACP doctor [--fix] # Health-check all integrations
88
+ jiraACP update-context # Regenerate CLAUDE.md from codebase scan
89
+ jiraACP projects list|add|remove # Manage configured projects
90
+ ```
91
+
92
+ ### Pipeline
93
+
94
+ ```bash
95
+ jiraACP run <ticketKey> [options]
96
+ --project <name> Target project (default: auto-detect from git)
97
+ --from <stage> Start from this stage
98
+ --to <stage> End at this stage
99
+ --dry-run Simulate without side effects
100
+ --no-confirm Non-interactive mode
101
+
102
+ jiraACP sprint [--project] [--sprint] [--parallel 2] [--filter <jql>] [--dry-run]
103
+ jiraACP triage [--project] [--sprint]
104
+ ```
105
+
106
+ ### Monitoring
107
+
108
+ ```bash
109
+ jiraACP status [ticketKey] # Daemon status, or pipeline state for a ticket
110
+ jiraACP logs [ticketKey] [-f] # Server logs, or ticket pipeline logs
111
+ jiraACP dashboard [--watch] # Terminal UI of active runs
112
+ jiraACP replay <ticketKey> # Replay completed run event log
113
+ jiraACP usage [--month YYYY-MM] # Token cost report per project
114
+ ```
115
+
116
+ ### Control
117
+
118
+ ```bash
119
+ jiraACP pause <ticketKey>
120
+ jiraACP resume <ticketKey>
121
+ jiraACP abort <ticketKey> [--reason]
122
+ jiraACP cancel-clarification <ticketKey>
123
+ ```
124
+
125
+ ### Config
126
+
127
+ ```bash
128
+ jiraACP config get [<key>]
129
+ jiraACP config set <key> <value>
130
+ jiraACP config edit
131
+ ```
132
+
133
+ ### Schedule
134
+
135
+ ```bash
136
+ jiraACP schedule add --cron "0 9 * * 1-5" --project <name>
137
+ jiraACP schedule list
138
+ jiraACP schedule remove <id>
139
+ ```
140
+
141
+ ### Utilities
142
+
143
+ ```bash
144
+ jiraACP serve [--port 3100] # Foreground webhook server (for dev/Docker)
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Configuration
150
+
151
+ Configs live at `~/.jira-acp/`:
152
+
153
+ ```
154
+ ~/.jira-acp/
155
+ ├── config.json # Global shared settings (tokens, pipeline defaults)
156
+ └── projects/
157
+ └── my-project.json # Per-project overrides
158
+ ```
159
+
160
+ ### Global config (`~/.jira-acp/config.json`)
161
+
162
+ Shared settings across all projects:
163
+
164
+ ```json
165
+ {
166
+ "telegram": { "botToken": "env:TELEGRAM_BOT_TOKEN" },
167
+ "github": { "token": "env:GITHUB_TOKEN" },
168
+ "pipeline": {
169
+ "maxCostUsdPerRun": 2.0,
170
+ "skipClarificationIfClear": true,
171
+ "failOnDeployFailure": true
172
+ }
173
+ }
174
+ ```
175
+
176
+ ### Project config (`~/.jira-acp/projects/<name>.json`)
177
+
178
+ Project-specific settings (override global):
179
+
180
+ ```json
181
+ {
182
+ "name": "my-saas",
183
+ "jira": {
184
+ "url": "https://myteam.atlassian.net",
185
+ "email": "dev@myteam.com",
186
+ "token": "env:JIRA_TOKEN",
187
+ "projectKey": "PROJ",
188
+ "clarityScoreThreshold": 0.7
189
+ },
190
+ "github": {
191
+ "owner": "myorg",
192
+ "repo": "my-saas",
193
+ "defaultBranch": "main",
194
+ "autoMergeStrategy": "squash"
195
+ },
196
+ "workspace": {
197
+ "rootDir": "/path/to/codebase"
198
+ },
199
+ "telegram": {
200
+ "chatId": "-1001234567890"
201
+ }
202
+ }
203
+ ```
204
+
205
+ Use `"env:VAR_NAME"` for any sensitive value — safe to commit the config file.
206
+
207
+ ---
208
+
209
+ ## Human-in-the-Loop
210
+
211
+ jiraACP contacts you on Telegram when a decision is needed:
212
+
213
+ | Trigger | What you get |
214
+ |---------|-------------|
215
+ | Clarity score below threshold | Questions + `/answer PROJ-123` template |
216
+ | PR review: major issues | PR diff + Approve / Reject buttons |
217
+ | Merge conflict | Conflicting files + "I'll resolve" button |
218
+ | Tests fail after 3 retries | Playwright screenshot + error + Re-run button |
219
+ | Cost about to exceed limit | "Continue / Abort?" prompt |
220
+
221
+ **Clarification timeout flow** (default: 1 hour):
222
+ - T+30m — reminder
223
+ - T+45m — "Pipeline skips in 15 min"
224
+ - T+60m — execute `clarificationTimeoutAction` (`skip` / `abort` / `proceed-with-warning`)
225
+
226
+ ### Telegram Commands
227
+
228
+ | Command | Action |
229
+ |---------|--------|
230
+ | `/run <ticketKey>` | Start pipeline for a ticket |
231
+ | `/abort <ticketKey>` | Abort running pipeline |
232
+ | `/resume <ticketKey>` | Resume paused pipeline |
233
+ | `/status` | Active pipeline states |
234
+ | `/logs` | Recent server log |
235
+ | `/tickets` | Open tickets grouped by release version |
236
+ | `/ticket <key>` | Ticket detail |
237
+ | `/projects` | Configured projects |
238
+ | `/archive <key>` | Close ticket topic |
239
+ | `/verbosity low\|medium\|high` | Notification verbosity |
240
+
241
+ ---
242
+
243
+ ## Crash Recovery
244
+
245
+ Pipelines survive crashes. State is an append-only event log:
246
+
247
+ ```bash
248
+ # Process killed mid-way through Stage 4 (Code)
249
+ jiraACP resume PROJ-123
250
+ # Detects dead lock, offers: "Resume from 'code'? [Y/n]"
251
+ # Replays events, restores context, restarts from Stage 4
252
+ ```
253
+
254
+ State stored per-ticket at `~/.jira-acp/runs/<project>/<ticketKey>/`.
255
+
256
+ ---
257
+
258
+ ## Security
259
+
260
+ - **No plaintext secrets** — use `"env:VAR_NAME"` in config. `jiraACP doctor` warns on plaintext tokens.
261
+ - **Minimal env forwarding** — agents receive only `PATH`, `HOME`, `ANTHROPIC_API_KEY`, project vars. Never full `process.env`.
262
+ - **No shell injection** — all subprocess calls use `spawn(cmd, argsArray)`, never `exec(templateString)`.
263
+ - **Agent path scoping** — writes restricted to `workspace.allowedPaths`.
264
+ - **Telegram filtering** — only messages from configured `chatId` accepted.
265
+ - **Atomic locks** — `O_EXCL` open flag, crash-safe, no zombie pipelines.
266
+
267
+ ---
268
+
269
+ ## Requirements
270
+
271
+ | Tool | Version | Purpose |
272
+ |------|---------|---------|
273
+ | Node.js | >= 20 | Runtime |
274
+ | [Claude Code](https://claude.ai/code) | latest | AI coding agent |
275
+ | Jira Cloud | — | Ticket source |
276
+ | GitHub | — | PR target |
277
+ | Telegram Bot | — | Notifications + human-in-the-loop |
278
+
279
+ ---
280
+
281
+ ## License
282
+
283
+ MIT
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ readLockData
4
+ } from "./chunk-FLPIU2QO.js";
5
+ import {
6
+ createTelegramNotifier
7
+ } from "./chunk-PVKVCUNR.js";
8
+ import "./chunk-RZK74PDF.js";
9
+ import {
10
+ StateManager,
11
+ getLockPath,
12
+ getRunDir
13
+ } from "./chunk-VWBCDZWQ.js";
14
+ import {
15
+ loadConfig
16
+ } from "./chunk-3YHD4SIN.js";
17
+ import "./chunk-LIEW4ULF.js";
18
+ import "./chunk-3U373M37.js";
19
+ import "./chunk-OJ4CNF73.js";
20
+ import {
21
+ createLogger
22
+ } from "./chunk-IT74N3UH.js";
23
+
24
+ // src/commands/abort.ts
25
+ import fs from "fs";
26
+ async function abortPipeline(ticketKey, projectName, reason) {
27
+ const logger = createLogger("abort");
28
+ const config = loadConfig(projectName);
29
+ const runDir = getRunDir(projectName, ticketKey);
30
+ const lockPath = getLockPath(projectName, ticketKey);
31
+ const stateFile = `${runDir}/state.json`;
32
+ if (!fs.existsSync(stateFile)) {
33
+ logger.error({ ticketKey }, "No pipeline run found for ticket");
34
+ process.stderr.write(`No pipeline run found for ${ticketKey}
35
+ `);
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ const state = new StateManager(runDir).current;
40
+ if (state.isCompleted || state.isAborted) {
41
+ process.stdout.write(
42
+ `Pipeline for ${ticketKey} is already ${state.isCompleted ? "completed" : "aborted"}
43
+ `
44
+ );
45
+ return;
46
+ }
47
+ const lockData = readLockData(lockPath);
48
+ const abortReason = reason ?? "Manually aborted by user";
49
+ if (lockData) {
50
+ try {
51
+ process.kill(lockData.pid, "SIGTERM");
52
+ logger.info(
53
+ { ticketKey, pid: lockData.pid },
54
+ "SIGTERM sent to pipeline process"
55
+ );
56
+ } catch (err) {
57
+ const isESRCH = err instanceof Error && "code" in err && err.code === "ESRCH";
58
+ if (!isESRCH) throw err;
59
+ logger.warn(
60
+ { ticketKey, pid: lockData.pid },
61
+ "Process already dead \u2014 recording abort"
62
+ );
63
+ }
64
+ } else {
65
+ logger.warn(
66
+ { ticketKey },
67
+ "No lock file found \u2014 recording abort event only"
68
+ );
69
+ }
70
+ const stateManager = new StateManager(runDir);
71
+ const currentState = stateManager.current;
72
+ if (!currentState.isAborted) {
73
+ stateManager.emit({ type: "PIPELINE_ABORTED", reason: abortReason });
74
+ logger.info(
75
+ { ticketKey, reason: abortReason },
76
+ "PIPELINE_ABORTED event recorded"
77
+ );
78
+ }
79
+ try {
80
+ const notifier = createTelegramNotifier(
81
+ config.telegram.botToken,
82
+ config.telegram.chatId,
83
+ ticketKey,
84
+ projectName,
85
+ config.telegram.topicId
86
+ );
87
+ await notifier.sendError(
88
+ ticketKey,
89
+ new Error(`Pipeline aborted: ${abortReason}`)
90
+ );
91
+ } catch {
92
+ logger.warn(
93
+ { ticketKey },
94
+ "Telegram notification failed during abort \u2014 continuing"
95
+ );
96
+ }
97
+ process.stdout.write(`\u2713 Pipeline for ${ticketKey} aborted: ${abortReason}
98
+ `);
99
+ }
100
+ export {
101
+ abortPipeline
102
+ };
103
+ //# sourceMappingURL=abort-GQE4OI5S.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/abort.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport { loadConfig } from \"../config/loader.js\";\nimport { getRunDir, getLockPath, StateManager } from \"../pipeline/state.js\";\nimport { readLockData } from \"../utils/lock.js\";\nimport { createLogger } from \"../utils/logger.js\";\nimport { createTelegramNotifier } from \"../integrations/telegram/notifier.js\";\n\nexport async function abortPipeline(\n ticketKey: string,\n projectName: string,\n reason?: string,\n): Promise<void> {\n const logger = createLogger(\"abort\");\n const config = loadConfig(projectName);\n\n const runDir = getRunDir(projectName, ticketKey);\n const lockPath = getLockPath(projectName, ticketKey);\n\n // Check state first\n const stateFile = `${runDir}/state.json`;\n if (!fs.existsSync(stateFile)) {\n logger.error({ ticketKey }, \"No pipeline run found for ticket\");\n process.stderr.write(`No pipeline run found for ${ticketKey}\\n`);\n process.exitCode = 1;\n return;\n }\n\n const state = new StateManager(runDir).current;\n if (state.isCompleted || state.isAborted) {\n process.stdout.write(\n `Pipeline for ${ticketKey} is already ${state.isCompleted ? \"completed\" : \"aborted\"}\\n`,\n );\n return;\n }\n\n // Read lock to get PID\n const lockData = readLockData(lockPath);\n const abortReason = reason ?? \"Manually aborted by user\";\n\n if (lockData) {\n try {\n process.kill(lockData.pid, \"SIGTERM\");\n logger.info(\n { ticketKey, pid: lockData.pid },\n \"SIGTERM sent to pipeline process\",\n );\n } catch (err: unknown) {\n // ESRCH = process not found — already dead\n const isESRCH =\n err instanceof Error &&\n \"code\" in err &&\n (err as NodeJS.ErrnoException).code === \"ESRCH\";\n if (!isESRCH) throw err;\n logger.warn(\n { ticketKey, pid: lockData.pid },\n \"Process already dead — recording abort\",\n );\n }\n } else {\n logger.warn(\n { ticketKey },\n \"No lock file found — recording abort event only\",\n );\n }\n\n // Emit abort event directly to state file (the killed process may not have time to)\n const stateManager = new StateManager(runDir);\n const currentState = stateManager.current;\n if (!currentState.isAborted) {\n stateManager.emit({ type: \"PIPELINE_ABORTED\", reason: abortReason });\n logger.info(\n { ticketKey, reason: abortReason },\n \"PIPELINE_ABORTED event recorded\",\n );\n }\n\n // Notify Telegram\n try {\n const notifier = createTelegramNotifier(\n config.telegram.botToken,\n config.telegram.chatId,\n ticketKey,\n projectName,\n config.telegram.topicId,\n );\n await notifier.sendError(\n ticketKey,\n new Error(`Pipeline aborted: ${abortReason}`),\n );\n } catch {\n logger.warn(\n { ticketKey },\n \"Telegram notification failed during abort — continuing\",\n );\n }\n\n process.stdout.write(`✓ Pipeline for ${ticketKey} aborted: ${abortReason}\\n`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA,OAAO,QAAQ;AAOf,eAAsB,cACpB,WACA,aACA,QACe;AACf,QAAM,SAAS,aAAa,OAAO;AACnC,QAAM,SAAS,WAAW,WAAW;AAErC,QAAM,SAAS,UAAU,aAAa,SAAS;AAC/C,QAAM,WAAW,YAAY,aAAa,SAAS;AAGnD,QAAM,YAAY,GAAG,MAAM;AAC3B,MAAI,CAAC,GAAG,WAAW,SAAS,GAAG;AAC7B,WAAO,MAAM,EAAE,UAAU,GAAG,kCAAkC;AAC9D,YAAQ,OAAO,MAAM,6BAA6B,SAAS;AAAA,CAAI;AAC/D,YAAQ,WAAW;AACnB;AAAA,EACF;AAEA,QAAM,QAAQ,IAAI,aAAa,MAAM,EAAE;AACvC,MAAI,MAAM,eAAe,MAAM,WAAW;AACxC,YAAQ,OAAO;AAAA,MACb,gBAAgB,SAAS,eAAe,MAAM,cAAc,cAAc,SAAS;AAAA;AAAA,IACrF;AACA;AAAA,EACF;AAGA,QAAM,WAAW,aAAa,QAAQ;AACtC,QAAM,cAAc,UAAU;AAE9B,MAAI,UAAU;AACZ,QAAI;AACF,cAAQ,KAAK,SAAS,KAAK,SAAS;AACpC,aAAO;AAAA,QACL,EAAE,WAAW,KAAK,SAAS,IAAI;AAAA,QAC/B;AAAA,MACF;AAAA,IACF,SAAS,KAAc;AAErB,YAAM,UACJ,eAAe,SACf,UAAU,OACT,IAA8B,SAAS;AAC1C,UAAI,CAAC,QAAS,OAAM;AACpB,aAAO;AAAA,QACL,EAAE,WAAW,KAAK,SAAS,IAAI;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AAAA,EACF,OAAO;AACL,WAAO;AAAA,MACL,EAAE,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAGA,QAAM,eAAe,IAAI,aAAa,MAAM;AAC5C,QAAM,eAAe,aAAa;AAClC,MAAI,CAAC,aAAa,WAAW;AAC3B,iBAAa,KAAK,EAAE,MAAM,oBAAoB,QAAQ,YAAY,CAAC;AACnE,WAAO;AAAA,MACL,EAAE,WAAW,QAAQ,YAAY;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAGA,MAAI;AACF,UAAM,WAAW;AAAA,MACf,OAAO,SAAS;AAAA,MAChB,OAAO,SAAS;AAAA,MAChB;AAAA,MACA;AAAA,MACA,OAAO,SAAS;AAAA,IAClB;AACA,UAAM,SAAS;AAAA,MACb;AAAA,MACA,IAAI,MAAM,qBAAqB,WAAW,EAAE;AAAA,IAC9C;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,MACL,EAAE,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAEA,UAAQ,OAAO,MAAM,uBAAkB,SAAS,aAAa,WAAW;AAAA,CAAI;AAC9E;","names":[]}
@@ -0,0 +1,96 @@
1
+ import {
2
+ loadConfig
3
+ } from "./chunk-MMWQHH25.js";
4
+ import {
5
+ createTelegramNotifier,
6
+ readLockData
7
+ } from "./chunk-WEJCTFQB.js";
8
+ import {
9
+ StateManager,
10
+ getLockPath,
11
+ getRunDir
12
+ } from "./chunk-BM4R6NST.js";
13
+ import {
14
+ createLogger
15
+ } from "./chunk-M4V3YOCY.js";
16
+
17
+ // src/commands/abort.ts
18
+ import fs from "fs";
19
+ async function abortPipeline(ticketKey, projectName, reason) {
20
+ const logger = createLogger("abort");
21
+ const config = loadConfig(projectName);
22
+ const runDir = getRunDir(projectName, ticketKey);
23
+ const lockPath = getLockPath(projectName, ticketKey);
24
+ const stateFile = `${runDir}/state.json`;
25
+ if (!fs.existsSync(stateFile)) {
26
+ logger.error({ ticketKey }, "No pipeline run found for ticket");
27
+ process.stderr.write(`No pipeline run found for ${ticketKey}
28
+ `);
29
+ process.exitCode = 1;
30
+ return;
31
+ }
32
+ const state = new StateManager(runDir).current;
33
+ if (state.isCompleted || state.isAborted) {
34
+ process.stdout.write(
35
+ `Pipeline for ${ticketKey} is already ${state.isCompleted ? "completed" : "aborted"}
36
+ `
37
+ );
38
+ return;
39
+ }
40
+ const lockData = readLockData(lockPath);
41
+ const abortReason = reason ?? "Manually aborted by user";
42
+ if (lockData) {
43
+ try {
44
+ process.kill(lockData.pid, "SIGTERM");
45
+ logger.info(
46
+ { ticketKey, pid: lockData.pid },
47
+ "SIGTERM sent to pipeline process"
48
+ );
49
+ } catch (err) {
50
+ const isESRCH = err instanceof Error && "code" in err && err.code === "ESRCH";
51
+ if (!isESRCH) throw err;
52
+ logger.warn(
53
+ { ticketKey, pid: lockData.pid },
54
+ "Process already dead \u2014 recording abort"
55
+ );
56
+ }
57
+ } else {
58
+ logger.warn(
59
+ { ticketKey },
60
+ "No lock file found \u2014 recording abort event only"
61
+ );
62
+ }
63
+ const stateManager = new StateManager(runDir);
64
+ const currentState = stateManager.current;
65
+ if (!currentState.isAborted) {
66
+ stateManager.emit({ type: "PIPELINE_ABORTED", reason: abortReason });
67
+ logger.info(
68
+ { ticketKey, reason: abortReason },
69
+ "PIPELINE_ABORTED event recorded"
70
+ );
71
+ }
72
+ try {
73
+ const notifier = createTelegramNotifier(
74
+ config.telegram.botToken,
75
+ config.telegram.chatId,
76
+ ticketKey,
77
+ projectName,
78
+ config.telegram.topicId
79
+ );
80
+ await notifier.sendError(
81
+ ticketKey,
82
+ new Error(`Pipeline aborted: ${abortReason}`)
83
+ );
84
+ } catch {
85
+ logger.warn(
86
+ { ticketKey },
87
+ "Telegram notification failed during abort \u2014 continuing"
88
+ );
89
+ }
90
+ process.stdout.write(`\u2713 Pipeline for ${ticketKey} aborted: ${abortReason}
91
+ `);
92
+ }
93
+ export {
94
+ abortPipeline
95
+ };
96
+ //# sourceMappingURL=abort-VMRQOADY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/abort.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport { loadConfig } from \"../config/loader.js\";\nimport { getRunDir, getLockPath, StateManager } from \"../pipeline/state.js\";\nimport { readLockData } from \"../utils/lock.js\";\nimport { createLogger } from \"../utils/logger.js\";\nimport { createTelegramNotifier } from \"../integrations/telegram/notifier.js\";\n\nexport async function abortPipeline(\n ticketKey: string,\n projectName: string,\n reason?: string,\n): Promise<void> {\n const logger = createLogger(\"abort\");\n const config = loadConfig(projectName);\n\n const runDir = getRunDir(projectName, ticketKey);\n const lockPath = getLockPath(projectName, ticketKey);\n\n // Check state first\n const stateFile = `${runDir}/state.json`;\n if (!fs.existsSync(stateFile)) {\n logger.error({ ticketKey }, \"No pipeline run found for ticket\");\n process.stderr.write(`No pipeline run found for ${ticketKey}\\n`);\n process.exitCode = 1;\n return;\n }\n\n const state = new StateManager(runDir).current;\n if (state.isCompleted || state.isAborted) {\n process.stdout.write(\n `Pipeline for ${ticketKey} is already ${state.isCompleted ? \"completed\" : \"aborted\"}\\n`,\n );\n return;\n }\n\n // Read lock to get PID\n const lockData = readLockData(lockPath);\n const abortReason = reason ?? \"Manually aborted by user\";\n\n if (lockData) {\n try {\n process.kill(lockData.pid, \"SIGTERM\");\n logger.info(\n { ticketKey, pid: lockData.pid },\n \"SIGTERM sent to pipeline process\",\n );\n } catch (err: unknown) {\n // ESRCH = process not found — already dead\n const isESRCH =\n err instanceof Error &&\n \"code\" in err &&\n (err as NodeJS.ErrnoException).code === \"ESRCH\";\n if (!isESRCH) throw err;\n logger.warn(\n { ticketKey, pid: lockData.pid },\n \"Process already dead — recording abort\",\n );\n }\n } else {\n logger.warn(\n { ticketKey },\n \"No lock file found — recording abort event only\",\n );\n }\n\n // Emit abort event directly to state file (the killed process may not have time to)\n const stateManager = new StateManager(runDir);\n const currentState = stateManager.current;\n if (!currentState.isAborted) {\n stateManager.emit({ type: \"PIPELINE_ABORTED\", reason: abortReason });\n logger.info(\n { ticketKey, reason: abortReason },\n \"PIPELINE_ABORTED event recorded\",\n );\n }\n\n // Notify Telegram\n try {\n const notifier = createTelegramNotifier(\n config.telegram.botToken,\n config.telegram.chatId,\n ticketKey,\n projectName,\n config.telegram.topicId,\n );\n await notifier.sendError(\n ticketKey,\n new Error(`Pipeline aborted: ${abortReason}`),\n );\n } catch {\n logger.warn(\n { ticketKey },\n \"Telegram notification failed during abort — continuing\",\n );\n }\n\n process.stdout.write(`✓ Pipeline for ${ticketKey} aborted: ${abortReason}\\n`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAAA,OAAO,QAAQ;AAOf,eAAsB,cACpB,WACA,aACA,QACe;AACf,QAAM,SAAS,aAAa,OAAO;AACnC,QAAM,SAAS,WAAW,WAAW;AAErC,QAAM,SAAS,UAAU,aAAa,SAAS;AAC/C,QAAM,WAAW,YAAY,aAAa,SAAS;AAGnD,QAAM,YAAY,GAAG,MAAM;AAC3B,MAAI,CAAC,GAAG,WAAW,SAAS,GAAG;AAC7B,WAAO,MAAM,EAAE,UAAU,GAAG,kCAAkC;AAC9D,YAAQ,OAAO,MAAM,6BAA6B,SAAS;AAAA,CAAI;AAC/D,YAAQ,WAAW;AACnB;AAAA,EACF;AAEA,QAAM,QAAQ,IAAI,aAAa,MAAM,EAAE;AACvC,MAAI,MAAM,eAAe,MAAM,WAAW;AACxC,YAAQ,OAAO;AAAA,MACb,gBAAgB,SAAS,eAAe,MAAM,cAAc,cAAc,SAAS;AAAA;AAAA,IACrF;AACA;AAAA,EACF;AAGA,QAAM,WAAW,aAAa,QAAQ;AACtC,QAAM,cAAc,UAAU;AAE9B,MAAI,UAAU;AACZ,QAAI;AACF,cAAQ,KAAK,SAAS,KAAK,SAAS;AACpC,aAAO;AAAA,QACL,EAAE,WAAW,KAAK,SAAS,IAAI;AAAA,QAC/B;AAAA,MACF;AAAA,IACF,SAAS,KAAc;AAErB,YAAM,UACJ,eAAe,SACf,UAAU,OACT,IAA8B,SAAS;AAC1C,UAAI,CAAC,QAAS,OAAM;AACpB,aAAO;AAAA,QACL,EAAE,WAAW,KAAK,SAAS,IAAI;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AAAA,EACF,OAAO;AACL,WAAO;AAAA,MACL,EAAE,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAGA,QAAM,eAAe,IAAI,aAAa,MAAM;AAC5C,QAAM,eAAe,aAAa;AAClC,MAAI,CAAC,aAAa,WAAW;AAC3B,iBAAa,KAAK,EAAE,MAAM,oBAAoB,QAAQ,YAAY,CAAC;AACnE,WAAO;AAAA,MACL,EAAE,WAAW,QAAQ,YAAY;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAGA,MAAI;AACF,UAAM,WAAW;AAAA,MACf,OAAO,SAAS;AAAA,MAChB,OAAO,SAAS;AAAA,MAChB;AAAA,MACA;AAAA,MACA,OAAO,SAAS;AAAA,IAClB;AACA,UAAM,SAAS;AAAA,MACb;AAAA,MACA,IAAI,MAAM,qBAAqB,WAAW,EAAE;AAAA,IAC9C;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,MACL,EAAE,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAEA,UAAQ,OAAO,MAAM,uBAAkB,SAAS,aAAa,WAAW;AAAA,CAAI;AAC9E;","names":[]}
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getBot,
4
+ initBot,
5
+ resetBot
6
+ } from "./chunk-OJ4CNF73.js";
7
+ import "./chunk-IT74N3UH.js";
8
+ export {
9
+ getBot,
10
+ initBot,
11
+ resetBot
12
+ };
13
+ //# sourceMappingURL=bot-WOTETAJY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ StateManager,
4
+ getRunDir
5
+ } from "./chunk-VWBCDZWQ.js";
6
+ import {
7
+ loadConfig
8
+ } from "./chunk-3YHD4SIN.js";
9
+ import "./chunk-LIEW4ULF.js";
10
+ import {
11
+ createLogger
12
+ } from "./chunk-IT74N3UH.js";
13
+
14
+ // src/commands/cancel-clarification.ts
15
+ import fs from "fs";
16
+ import path from "path";
17
+ import os from "os";
18
+ var PENDING_FILE = path.join(
19
+ os.homedir(),
20
+ ".jira-acp",
21
+ "pending-clarifications.json"
22
+ );
23
+ async function cancelClarification(ticketKey, projectName) {
24
+ const logger = createLogger("cancel-clarification");
25
+ loadConfig(projectName);
26
+ let removed = false;
27
+ if (fs.existsSync(PENDING_FILE)) {
28
+ const raw = JSON.parse(fs.readFileSync(PENDING_FILE, "utf8"));
29
+ const filtered = raw.filter((e) => e.ticketKey !== ticketKey);
30
+ if (filtered.length < raw.length) {
31
+ const tmpPath = PENDING_FILE + ".tmp";
32
+ fs.writeFileSync(tmpPath, JSON.stringify(filtered, null, 2));
33
+ fs.renameSync(tmpPath, PENDING_FILE);
34
+ removed = true;
35
+ logger.info({ ticketKey }, "Removed from pending-clarifications");
36
+ }
37
+ }
38
+ if (!removed) {
39
+ process.stdout.write(`No pending clarification found for ${ticketKey}
40
+ `);
41
+ return;
42
+ }
43
+ const runDir = getRunDir(projectName, ticketKey);
44
+ if (fs.existsSync(runDir)) {
45
+ try {
46
+ new StateManager(runDir).emit({
47
+ type: "CLARIFICATION_RECEIVED",
48
+ answers: "CANCELLED"
49
+ });
50
+ logger.info({ ticketKey }, "CLARIFICATION_RECEIVED(CANCELLED) emitted");
51
+ } catch (err) {
52
+ logger.warn(
53
+ { ticketKey, err },
54
+ "Failed to emit CLARIFICATION_RECEIVED \u2014 non-fatal"
55
+ );
56
+ }
57
+ }
58
+ process.stdout.write(`\u2713 Clarification for ${ticketKey} cancelled
59
+ `);
60
+ }
61
+ export {
62
+ cancelClarification
63
+ };
64
+ //# sourceMappingURL=cancel-clarification-4G5S2HJZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/cancel-clarification.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\nimport { loadConfig } from \"../config/loader.js\";\nimport { getRunDir, StateManager } from \"../pipeline/state.js\";\nimport { createLogger } from \"../utils/logger.js\";\n\nconst PENDING_FILE = path.join(\n os.homedir(),\n \".jira-acp\",\n \"pending-clarifications.json\",\n);\n\nexport async function cancelClarification(\n ticketKey: string,\n projectName: string,\n): Promise<void> {\n const logger = createLogger(\"cancel-clarification\");\n loadConfig(projectName);\n\n let removed = false;\n if (fs.existsSync(PENDING_FILE)) {\n const raw = JSON.parse(fs.readFileSync(PENDING_FILE, \"utf8\")) as Array<{\n ticketKey: string;\n }>;\n const filtered = raw.filter((e) => e.ticketKey !== ticketKey);\n if (filtered.length < raw.length) {\n // Atomic write: write to temp file then rename to avoid race conditions\n const tmpPath = PENDING_FILE + \".tmp\";\n fs.writeFileSync(tmpPath, JSON.stringify(filtered, null, 2));\n fs.renameSync(tmpPath, PENDING_FILE);\n removed = true;\n logger.info({ ticketKey }, \"Removed from pending-clarifications\");\n }\n }\n\n if (!removed) {\n process.stdout.write(`No pending clarification found for ${ticketKey}\\n`);\n return;\n }\n\n // Emit CLARIFICATION_RECEIVED into state if run dir exists\n const runDir = getRunDir(projectName, ticketKey);\n if (fs.existsSync(runDir)) {\n try {\n new StateManager(runDir).emit({\n type: \"CLARIFICATION_RECEIVED\",\n answers: \"CANCELLED\",\n });\n logger.info({ ticketKey }, \"CLARIFICATION_RECEIVED(CANCELLED) emitted\");\n } catch (err) {\n logger.warn(\n { ticketKey, err },\n \"Failed to emit CLARIFICATION_RECEIVED — non-fatal\",\n );\n }\n }\n\n process.stdout.write(`✓ Clarification for ${ticketKey} cancelled\\n`);\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAKf,IAAM,eAAe,KAAK;AAAA,EACxB,GAAG,QAAQ;AAAA,EACX;AAAA,EACA;AACF;AAEA,eAAsB,oBACpB,WACA,aACe;AACf,QAAM,SAAS,aAAa,sBAAsB;AAClD,aAAW,WAAW;AAEtB,MAAI,UAAU;AACd,MAAI,GAAG,WAAW,YAAY,GAAG;AAC/B,UAAM,MAAM,KAAK,MAAM,GAAG,aAAa,cAAc,MAAM,CAAC;AAG5D,UAAM,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,cAAc,SAAS;AAC5D,QAAI,SAAS,SAAS,IAAI,QAAQ;AAEhC,YAAM,UAAU,eAAe;AAC/B,SAAG,cAAc,SAAS,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AAC3D,SAAG,WAAW,SAAS,YAAY;AACnC,gBAAU;AACV,aAAO,KAAK,EAAE,UAAU,GAAG,qCAAqC;AAAA,IAClE;AAAA,EACF;AAEA,MAAI,CAAC,SAAS;AACZ,YAAQ,OAAO,MAAM,sCAAsC,SAAS;AAAA,CAAI;AACxE;AAAA,EACF;AAGA,QAAM,SAAS,UAAU,aAAa,SAAS;AAC/C,MAAI,GAAG,WAAW,MAAM,GAAG;AACzB,QAAI;AACF,UAAI,aAAa,MAAM,EAAE,KAAK;AAAA,QAC5B,MAAM;AAAA,QACN,SAAS;AAAA,MACX,CAAC;AACD,aAAO,KAAK,EAAE,UAAU,GAAG,2CAA2C;AAAA,IACxE,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,EAAE,WAAW,IAAI;AAAA,QACjB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,UAAQ,OAAO,MAAM,4BAAuB,SAAS;AAAA,CAAc;AACrE;","names":[]}
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/integrations/telegram/topic-manager.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import os from "os";
7
+ var STORE_PATH = path.join(os.homedir(), ".jira-acp", "telegram-topics.json");
8
+ function loadStore() {
9
+ if (!fs.existsSync(STORE_PATH)) return {};
10
+ try {
11
+ return JSON.parse(fs.readFileSync(STORE_PATH, "utf8"));
12
+ } catch {
13
+ return {};
14
+ }
15
+ }
16
+ function saveStore(store) {
17
+ fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });
18
+ fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
19
+ }
20
+ async function getOrCreateTopic(bot, chatId, ticketKey, projectName) {
21
+ const store = loadStore();
22
+ const key = `${projectName}:${ticketKey}`;
23
+ if (store[key] !== void 0) return store[key];
24
+ try {
25
+ const topic = await bot.api.createForumTopic(chatId, ticketKey);
26
+ store[key] = topic.message_thread_id;
27
+ saveStore(store);
28
+ return topic.message_thread_id;
29
+ } catch {
30
+ return void 0;
31
+ }
32
+ }
33
+ async function getOrCreateSystemTopic(bot, chatId, projectName) {
34
+ const store = loadStore();
35
+ const key = `${projectName}:__notifications__`;
36
+ if (store[key] !== void 0) return store[key];
37
+ try {
38
+ const topic = await bot.api.createForumTopic(chatId, "\u{1F514} Notifications", {
39
+ icon_color: 7322096
40
+ // light blue
41
+ });
42
+ store[key] = topic.message_thread_id;
43
+ saveStore(store);
44
+ return topic.message_thread_id;
45
+ } catch {
46
+ return void 0;
47
+ }
48
+ }
49
+ async function archiveTopic(bot, chatId, ticketKey, projectName) {
50
+ const store = loadStore();
51
+ const key = `${projectName}:${ticketKey}`;
52
+ const threadId = store[key];
53
+ if (threadId === void 0) return;
54
+ try {
55
+ await bot.api.closeForumTopic(chatId, threadId);
56
+ } catch {
57
+ }
58
+ delete store[key];
59
+ saveStore(store);
60
+ }
61
+
62
+ export {
63
+ getOrCreateTopic,
64
+ getOrCreateSystemTopic,
65
+ archiveTopic
66
+ };
67
+ //# sourceMappingURL=chunk-3U373M37.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/integrations/telegram/topic-manager.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\nimport type { Bot } from \"grammy\";\n\nconst STORE_PATH = path.join(os.homedir(), \".jira-acp\", \"telegram-topics.json\");\n\ntype TopicStore = Record<string, number>; // \"projectName:ticketKey\" => message_thread_id\n\nfunction loadStore(): TopicStore {\n if (!fs.existsSync(STORE_PATH)) return {};\n try {\n return JSON.parse(fs.readFileSync(STORE_PATH, \"utf8\")) as TopicStore;\n } catch {\n return {};\n }\n}\n\nfunction saveStore(store: TopicStore): void {\n fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });\n fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));\n}\n\nexport async function getOrCreateTopic(\n bot: Bot,\n chatId: number | string,\n ticketKey: string,\n projectName: string,\n): Promise<number | undefined> {\n const store = loadStore();\n const key = `${projectName}:${ticketKey}`;\n\n if (store[key] !== undefined) return store[key];\n\n try {\n const topic = await bot.api.createForumTopic(chatId, ticketKey);\n store[key] = topic.message_thread_id;\n saveStore(store);\n return topic.message_thread_id;\n } catch {\n // Chat is not a forum supergroup — topic creation not supported\n return undefined;\n }\n}\n\n/** Get or create a persistent \"🔔 Notifications\" topic for system-level messages. */\nexport async function getOrCreateSystemTopic(\n bot: Bot,\n chatId: number | string,\n projectName: string,\n): Promise<number | undefined> {\n const store = loadStore();\n const key = `${projectName}:__notifications__`;\n\n if (store[key] !== undefined) return store[key];\n\n try {\n const topic = await bot.api.createForumTopic(chatId, \"🔔 Notifications\", {\n icon_color: 0x6fb9f0, // light blue\n });\n store[key] = topic.message_thread_id;\n saveStore(store);\n return topic.message_thread_id;\n } catch {\n return undefined;\n }\n}\n\nexport async function archiveTopic(\n bot: Bot,\n chatId: number | string,\n ticketKey: string,\n projectName: string,\n): Promise<void> {\n const store = loadStore();\n const key = `${projectName}:${ticketKey}`;\n const threadId = store[key];\n if (threadId === undefined) return;\n\n try {\n await bot.api.closeForumTopic(chatId, threadId);\n } catch {\n // Topic may already be closed or deleted\n }\n\n delete store[key];\n saveStore(store);\n}\n"],"mappings":";;;AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAGf,IAAM,aAAa,KAAK,KAAK,GAAG,QAAQ,GAAG,aAAa,sBAAsB;AAI9E,SAAS,YAAwB;AAC/B,MAAI,CAAC,GAAG,WAAW,UAAU,EAAG,QAAO,CAAC;AACxC,MAAI;AACF,WAAO,KAAK,MAAM,GAAG,aAAa,YAAY,MAAM,CAAC;AAAA,EACvD,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,SAAS,UAAU,OAAyB;AAC1C,KAAG,UAAU,KAAK,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AAC1D,KAAG,cAAc,YAAY,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAC7D;AAEA,eAAsB,iBACpB,KACA,QACA,WACA,aAC6B;AAC7B,QAAM,QAAQ,UAAU;AACxB,QAAM,MAAM,GAAG,WAAW,IAAI,SAAS;AAEvC,MAAI,MAAM,GAAG,MAAM,OAAW,QAAO,MAAM,GAAG;AAE9C,MAAI;AACF,UAAM,QAAQ,MAAM,IAAI,IAAI,iBAAiB,QAAQ,SAAS;AAC9D,UAAM,GAAG,IAAI,MAAM;AACnB,cAAU,KAAK;AACf,WAAO,MAAM;AAAA,EACf,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,uBACpB,KACA,QACA,aAC6B;AAC7B,QAAM,QAAQ,UAAU;AACxB,QAAM,MAAM,GAAG,WAAW;AAE1B,MAAI,MAAM,GAAG,MAAM,OAAW,QAAO,MAAM,GAAG;AAE9C,MAAI;AACF,UAAM,QAAQ,MAAM,IAAI,IAAI,iBAAiB,QAAQ,2BAAoB;AAAA,MACvE,YAAY;AAAA;AAAA,IACd,CAAC;AACD,UAAM,GAAG,IAAI,MAAM;AACnB,cAAU,KAAK;AACf,WAAO,MAAM;AAAA,EACf,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,aACpB,KACA,QACA,WACA,aACe;AACf,QAAM,QAAQ,UAAU;AACxB,QAAM,MAAM,GAAG,WAAW,IAAI,SAAS;AACvC,QAAM,WAAW,MAAM,GAAG;AAC1B,MAAI,aAAa,OAAW;AAE5B,MAAI;AACF,UAAM,IAAI,IAAI,gBAAgB,QAAQ,QAAQ;AAAA,EAChD,QAAQ;AAAA,EAER;AAEA,SAAO,MAAM,GAAG;AAChB,YAAU,KAAK;AACjB;","names":[]}