pi-harness-runtime 1.1.346 → 1.1.348

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 (125) hide show
  1. package/index.ts +71 -75
  2. package/package.json +1 -1
  3. package/packages/a2a-adapter/package.json +1 -1
  4. package/packages/agent-policy/package.json +1 -1
  5. package/packages/architecture-generator/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-refactor/package.json +1 -1
  8. package/packages/autonomous-runtime/package.json +1 -1
  9. package/packages/cache-strategy/package.json +1 -1
  10. package/packages/capability-registry/package.json +1 -1
  11. package/packages/checkpoint/package.json +1 -1
  12. package/packages/cli-plugin-sdk/package.json +1 -1
  13. package/packages/clipboard/package.json +1 -1
  14. package/packages/clipboard-plugin/package.json +1 -1
  15. package/packages/code-generation/package.json +1 -1
  16. package/packages/code-review/package.json +1 -1
  17. package/packages/codex-adapter/package.json +1 -1
  18. package/packages/codex-watcher/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/connection-runtime/package.json +1 -1
  21. package/packages/context-compiler/package.json +1 -1
  22. package/packages/context-discovery/package.json +1 -1
  23. package/packages/context-manager/package.json +1 -1
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/cookie-sanitizer/package.json +1 -1
  26. package/packages/cost-optimizer/package.json +1 -1
  27. package/packages/dependency-analyzer/package.json +1 -1
  28. package/packages/django-plugin/package.json +1 -1
  29. package/packages/doc-generator/package.json +1 -1
  30. package/packages/durable-review/package.json +1 -1
  31. package/packages/e2e-tester/package.json +1 -1
  32. package/packages/evaluation-engine/package.json +1 -1
  33. package/packages/evaluation-runner/package.json +1 -1
  34. package/packages/event-bus/package.json +1 -1
  35. package/packages/event-sink/package.json +1 -1
  36. package/packages/event-store/package.json +1 -1
  37. package/packages/experience-replay/package.json +1 -1
  38. package/packages/feedback-collector/package.json +1 -1
  39. package/packages/file-copy-helper/package.json +1 -1
  40. package/packages/framework-detector/package.json +1 -1
  41. package/packages/framework-plugin-sdk/package.json +1 -1
  42. package/packages/frappe-plugin/package.json +1 -1
  43. package/packages/generic-web-plugin/package.json +1 -1
  44. package/packages/health-monitor/package.json +1 -1
  45. package/packages/intent-analyzer/package.json +1 -1
  46. package/packages/jev-judge/package.json +1 -1
  47. package/packages/knowledge-graph/package.json +1 -1
  48. package/packages/knowledge-retrieval/package.json +1 -1
  49. package/packages/laravel-plugin/package.json +1 -1
  50. package/packages/learning-engine/package.json +1 -1
  51. package/packages/mcp-adapter/package.json +1 -1
  52. package/packages/memory-engine/package.json +1 -1
  53. package/packages/milestone-manager/package.json +1 -1
  54. package/packages/model-registry/package.json +1 -1
  55. package/packages/nextjs-plugin/package.json +1 -1
  56. package/packages/notification/dist/index.d.ts +1 -0
  57. package/packages/notification/dist/index.d.ts.map +1 -1
  58. package/packages/notification/dist/index.js +1 -0
  59. package/packages/notification/dist/index.js.map +1 -1
  60. package/packages/notification/dist/notification-center.d.ts +13 -0
  61. package/packages/notification/dist/notification-center.d.ts.map +1 -1
  62. package/packages/notification/dist/notification-center.js +91 -1
  63. package/packages/notification/dist/notification-center.js.map +1 -1
  64. package/packages/notification/dist/pending-question.d.ts +198 -0
  65. package/packages/notification/dist/pending-question.d.ts.map +1 -0
  66. package/packages/notification/dist/pending-question.js +410 -0
  67. package/packages/notification/dist/pending-question.js.map +1 -0
  68. package/packages/notification/package.json +1 -1
  69. package/packages/notification/src/index.ts +1 -0
  70. package/packages/notification/src/notification-center.ts +92 -1
  71. package/packages/notification/src/pending-question.ts +594 -0
  72. package/packages/notification/test/pending-question.test.ts +331 -0
  73. package/packages/observability/package.json +1 -1
  74. package/packages/okf-indexer/package.json +1 -1
  75. package/packages/paperclip-integration/package.json +1 -1
  76. package/packages/performance-optimizer/package.json +1 -1
  77. package/packages/permission-mode/package.json +1 -1
  78. package/packages/privilege-broker/package.json +1 -1
  79. package/packages/project-analyzer/package.json +1 -1
  80. package/packages/project-bootstrap/package.json +1 -1
  81. package/packages/projection-engine/package.json +1 -1
  82. package/packages/prompt-compiler/package.json +1 -1
  83. package/packages/prompt-versioning/package.json +1 -1
  84. package/packages/provider-adapter-sdk/package.json +1 -1
  85. package/packages/provider-router/package.json +1 -1
  86. package/packages/provider-selector/package.json +1 -1
  87. package/packages/providers/package.json +1 -1
  88. package/packages/qdrant-skills/package.json +1 -1
  89. package/packages/quota-manager/package.json +1 -1
  90. package/packages/rate-limiter/package.json +1 -1
  91. package/packages/react-vite-plugin/package.json +1 -1
  92. package/packages/relay/package.json +1 -1
  93. package/packages/release-manager/package.json +1 -1
  94. package/packages/requirement-compiler/package.json +1 -1
  95. package/packages/runtime/package.json +1 -1
  96. package/packages/scheduler/package.json +1 -1
  97. package/packages/scheduler-adapter/package.json +1 -1
  98. package/packages/session/package.json +1 -1
  99. package/packages/session-api/package.json +1 -1
  100. package/packages/session-export/package.json +1 -1
  101. package/packages/shared-context/package.json +1 -1
  102. package/packages/skill-mcp-client/package.json +1 -1
  103. package/packages/skill-registry/package.json +1 -1
  104. package/packages/skills/package.json +1 -1
  105. package/packages/sprint-planner/package.json +1 -1
  106. package/packages/subscription-engine/package.json +1 -1
  107. package/packages/task-compiler/package.json +1 -1
  108. package/packages/telegram/package.json +1 -1
  109. package/packages/tencentdb-memory/package.json +1 -1
  110. package/packages/tencentdb-sync/package.json +1 -1
  111. package/packages/test-data-generator/package.json +1 -1
  112. package/packages/test-generator/package.json +1 -1
  113. package/packages/todo-bd-sync/package.json +1 -1
  114. package/packages/token-estimation/package.json +1 -1
  115. package/packages/token-optimizer/package.json +1 -1
  116. package/packages/trajectory/package.json +1 -1
  117. package/packages/tui/package.json +1 -1
  118. package/packages/types/package.json +1 -1
  119. package/packages/voice-api/package.json +1 -1
  120. package/packages/webmcp-tester/package.json +1 -1
  121. package/packages/wiki/package.json +1 -1
  122. package/packages/workflow-events/package.json +1 -1
  123. package/packages/workspace-scanner/package.json +1 -1
  124. package/packages/worktree/package.json +1 -1
  125. package/packages/write-review/package.json +1 -1
@@ -0,0 +1,410 @@
1
+ /**
2
+ * Pending Question Controller
3
+ *
4
+ * Manages pending questions that require human response.
5
+ * Supports multiple channels (Telegram, LINE) and Jev evaluation.
6
+ */
7
+ import { logWarn } from "./file-logger.js";
8
+ /**
9
+ * Pending Question Controller
10
+ */
11
+ export class PendingQuestionController {
12
+ questions = new Map();
13
+ currentQuestion = null;
14
+ jevDecisionCallback;
15
+ userAnswerCallback;
16
+ config;
17
+ constructor(config) {
18
+ this.config = {
19
+ evaluationDelayMs: config?.evaluationDelayMs ?? 5 * 60 * 1000, // 5 minutes
20
+ jevEnabled: config?.jevEnabled ?? true,
21
+ };
22
+ }
23
+ /**
24
+ * Set callback for Jev decisions
25
+ */
26
+ onJevDecision(callback) {
27
+ this.jevDecisionCallback = callback;
28
+ }
29
+ /**
30
+ * Set callback for user answers
31
+ */
32
+ onUserAnswer(callback) {
33
+ this.userAnswerCallback = callback;
34
+ }
35
+ /**
36
+ * Generate unique request ID
37
+ */
38
+ generateRequestId() {
39
+ return `q-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
40
+ }
41
+ /**
42
+ * Detect if text contains a question requiring response
43
+ */
44
+ detectQuestion(text, _options) {
45
+ // Find all question positions and extract each question sentence
46
+ const questionPositions = [];
47
+ let searchFrom = 0;
48
+ while (true) {
49
+ const questionIndex = text.indexOf('?', searchFrom);
50
+ if (questionIndex === -1)
51
+ break;
52
+ // Check if this question is inside quotes - if so, skip
53
+ const beforeQuestion = text.substring(0, questionIndex);
54
+ // Count unescaped quotes before and after
55
+ const openQuotes = (beforeQuestion.match(/"/g) || []).length;
56
+ const isInsideQuotes = openQuotes % 2 === 1;
57
+ if (isInsideQuotes) {
58
+ // This question is inside quotes - skip it
59
+ searchFrom = questionIndex + 1;
60
+ continue;
61
+ }
62
+ // Extract sentence for this question
63
+ let sentenceStart = searchFrom;
64
+ // Look backwards for sentence start
65
+ for (let i = questionIndex - 1; i >= searchFrom; i--) {
66
+ const char = text[i];
67
+ if (char === '.' || char === '!' || char === '\n') {
68
+ sentenceStart = i + 1;
69
+ break;
70
+ }
71
+ }
72
+ const sentence = text.substring(sentenceStart, questionIndex + 1).trim();
73
+ questionPositions.push({ start: sentenceStart, end: questionIndex, sentence });
74
+ searchFrom = questionIndex + 1;
75
+ }
76
+ // Check each question for approval/clarification first (priority)
77
+ for (const q of questionPositions) {
78
+ // Skip quoted questions
79
+ if (this.isQuotedOrCode(text, q.sentence)) {
80
+ continue;
81
+ }
82
+ const isRoutine = this.isRoutineContinuation(q.sentence);
83
+ const kind = this.determineKind(q.sentence, isRoutine);
84
+ // Return first approval or clarification question found
85
+ if (kind === "approval" || kind === "clarification") {
86
+ return {
87
+ detected: true,
88
+ questionText: q.sentence,
89
+ kind,
90
+ isRoutine,
91
+ };
92
+ }
93
+ }
94
+ // If no approval/clarification found, return first non-quoted routine question
95
+ for (const q of questionPositions) {
96
+ if (this.isQuotedOrCode(text, q.sentence)) {
97
+ continue;
98
+ }
99
+ const isRoutine = this.isRoutineContinuation(q.sentence);
100
+ return {
101
+ detected: true,
102
+ questionText: q.sentence,
103
+ kind: "routine",
104
+ isRoutine,
105
+ };
106
+ }
107
+ // No question found - check for approval patterns without ?
108
+ const approvalResult = this.checkApprovalPatterns(text);
109
+ if (approvalResult)
110
+ return approvalResult;
111
+ return { detected: false };
112
+ }
113
+ /**
114
+ * Check for approval patterns in text without question mark
115
+ * Only triggers if the text is short and looks like a question (no example: prefix)
116
+ */
117
+ checkApprovalPatterns(text) {
118
+ // Don't trigger on example/quotation text or code
119
+ const lower = text.toLowerCase();
120
+ if (/\b(example|such as|like this|for instance)\b/.test(lower)) {
121
+ return null;
122
+ }
123
+ // Don't trigger on code blocks
124
+ if (/```/.test(text)) {
125
+ return null;
126
+ }
127
+ // Only trigger if text is reasonably short (likely a real question, not a paragraph)
128
+ if (text.length > 200) {
129
+ return null;
130
+ }
131
+ const approvalPatterns = [
132
+ /\bwant me to\b/i,
133
+ /\bwould you like me to\b/i,
134
+ /\bshall i\b/i,
135
+ /\bshould i add\b/i,
136
+ /\bshould i implement\b/i,
137
+ /\bshould we add\b/i,
138
+ /\bshould we implement\b/i,
139
+ /\bdo you want me to\b/i,
140
+ /\bcan i (start|proceed|continue)\b/i,
141
+ ];
142
+ for (const pattern of approvalPatterns) {
143
+ if (pattern.test(text)) {
144
+ return {
145
+ detected: true,
146
+ questionText: text.trim(),
147
+ kind: "approval",
148
+ isRoutine: false,
149
+ };
150
+ }
151
+ }
152
+ return null;
153
+ }
154
+ /**
155
+ * Check if question is quoted or in code block
156
+ */
157
+ isQuotedOrCode(fullText, question) {
158
+ // Normalize question for comparison - remove surrounding quotes if present
159
+ let normalizedQuestion = question.trim();
160
+ // Check if the question itself starts with a quote
161
+ const startsWithQuote = normalizedQuestion.startsWith('"') || normalizedQuestion.startsWith("'");
162
+ if (startsWithQuote) {
163
+ return true;
164
+ }
165
+ // Check if question appears inside code block
166
+ const codeBlockPattern = /```[\s\S]*?```/g;
167
+ const codeBlocks = fullText.match(codeBlockPattern) || [];
168
+ for (const block of codeBlocks) {
169
+ // Check if the normalized question is inside this code block
170
+ if (block.includes(normalizedQuestion)) {
171
+ return true;
172
+ }
173
+ }
174
+ // Check if the question text appears inside quotes in the original full text
175
+ // This catches cases like: Here's an example: "Want me to implement this?"
176
+ // where the extracted sentence is "Here's an example: "Want me to implement this?"
177
+ // but we need to check if the CORE question is inside quotes
178
+ const questionWithoutQ = normalizedQuestion.replace(/\?$/, '');
179
+ // Look for the question text inside double quotes in the original text
180
+ // Pattern: find "...question...?" or "...question..."
181
+ const escapedQuestion = questionWithoutQ.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
182
+ const quotedQuestionPattern = new RegExp(`"[^"]*${escapedQuestion}[^"]*"`, 'i');
183
+ if (quotedQuestionPattern.test(fullText)) {
184
+ return true;
185
+ }
186
+ // Also check single quotes
187
+ const singleQuotedPattern = new RegExp(`'[^']*${escapedQuestion}[^']*'`, 'i');
188
+ if (singleQuotedPattern.test(fullText)) {
189
+ return true;
190
+ }
191
+ return false;
192
+ }
193
+ /**
194
+ * Determine question kind
195
+ */
196
+ determineKind(question, isRoutine) {
197
+ if (isRoutine)
198
+ return "routine";
199
+ const lower = question.toLowerCase();
200
+ // Clarification patterns (check first - more specific)
201
+ const clarifyPatterns = [
202
+ /\bdo you mean\b/i,
203
+ /\bwhich (one|option|approach)\b/i,
204
+ /\bshould i use\b/i,
205
+ /\bprefers?\b/i,
206
+ /\bwhich (one|option)\b/i,
207
+ ];
208
+ for (const pattern of clarifyPatterns) {
209
+ if (pattern.test(lower)) {
210
+ return "clarification";
211
+ }
212
+ }
213
+ // Explicit approval patterns (want me to, would you like me to, etc.)
214
+ const approvalPatterns = [
215
+ /\bwant me to\b/i,
216
+ /\bwould you like me to\b/i,
217
+ /\bshall i\b/i,
218
+ /\bshould i add\b/i,
219
+ /\bshould i implement\b/i,
220
+ /\bshould we add\b/i,
221
+ /\bshould we implement\b/i,
222
+ /\bdo you want me to\b/i,
223
+ /\bcan i (start|proceed|add|implement)\b/i,
224
+ ];
225
+ for (const pattern of approvalPatterns) {
226
+ if (pattern.test(lower)) {
227
+ return "approval";
228
+ }
229
+ }
230
+ return "routine";
231
+ }
232
+ /**
233
+ * Check if it's a routine continuation question
234
+ */
235
+ isRoutineContinuation(question) {
236
+ const lower = question.toLowerCase();
237
+ // Be specific - these patterns mean "continue with next task"
238
+ // Don't match approval patterns like "Would you like me to add X next?"
239
+ const routinePatterns = [
240
+ /\bshould i continue/i,
241
+ /\bshould we continue/i,
242
+ /\bcan i continue/i,
243
+ /\bcan we continue/i,
244
+ /\bmay i continue/i,
245
+ /\bshall i proceed/i,
246
+ /\bshould i proceed/i,
247
+ /^continue\?$/i,
248
+ /^proceed\?$/i,
249
+ /^next\?$/i,
250
+ /\bshall we (proceed|continue)/i,
251
+ /\bmay we (proceed|continue)/i,
252
+ /\bcan i proceed/i,
253
+ /\bshould i continue with/i,
254
+ /\bcan i proceed with/i,
255
+ ];
256
+ for (const pattern of routinePatterns) {
257
+ if (pattern.test(lower)) {
258
+ return true;
259
+ }
260
+ }
261
+ return false;
262
+ }
263
+ /**
264
+ * Create a new pending question
265
+ */
266
+ createQuestion(params) {
267
+ const requestId = this.generateRequestId();
268
+ const question = {
269
+ requestId,
270
+ sessionId: params.sessionId,
271
+ workspacePath: params.workspacePath,
272
+ jobId: params.jobId,
273
+ questionText: params.text,
274
+ kind: params.kind,
275
+ state: "pending",
276
+ createdAt: new Date(),
277
+ dueAt: new Date(Date.now() + this.config.evaluationDelayMs),
278
+ deliveryState: {},
279
+ userNotified: false,
280
+ revision: 1,
281
+ };
282
+ this.questions.set(requestId, question);
283
+ this.currentQuestion = question;
284
+ logWarn("[PendingQuestion]", `Created question ${requestId}: ${params.text.slice(0, 50)}...`);
285
+ return question;
286
+ }
287
+ /**
288
+ * Get current pending question
289
+ */
290
+ getCurrentQuestion() {
291
+ return this.currentQuestion;
292
+ }
293
+ /**
294
+ * Check if there's a pending question
295
+ */
296
+ hasPendingQuestion() {
297
+ return this.currentQuestion !== null &&
298
+ this.currentQuestion.state === "pending";
299
+ }
300
+ /**
301
+ * Update delivery state for a channel
302
+ */
303
+ updateDeliveryState(channel, delivered, error) {
304
+ if (!this.currentQuestion)
305
+ return;
306
+ this.currentQuestion.deliveryState[channel] = {
307
+ delivered,
308
+ deliveredAt: delivered ? new Date() : undefined,
309
+ error,
310
+ };
311
+ if (delivered) {
312
+ this.currentQuestion.userNotified = true;
313
+ }
314
+ }
315
+ /**
316
+ * Resolve the current question
317
+ */
318
+ resolve(params) {
319
+ if (!this.currentQuestion) {
320
+ logWarn("[PendingQuestion]", "No question to resolve");
321
+ return;
322
+ }
323
+ this.currentQuestion.state = "resolved";
324
+ this.currentQuestion.resolution = params.resolution;
325
+ this.currentQuestion.resolvedBy = params.by;
326
+ this.currentQuestion.resolvedAt = new Date();
327
+ logWarn("[PendingQuestion]", `Resolved question ${this.currentQuestion.requestId}: ${params.resolution} by ${params.by}`);
328
+ // Notify callback
329
+ if (this.userAnswerCallback) {
330
+ this.userAnswerCallback(this.currentQuestion, params.resolution);
331
+ }
332
+ }
333
+ /**
334
+ * Cancel the current question (e.g., on session switch)
335
+ */
336
+ cancel(reason = "cancelled") {
337
+ if (!this.currentQuestion)
338
+ return;
339
+ this.currentQuestion.state = "cancelled";
340
+ logWarn("[PendingQuestion]", `Cancelled question ${this.currentQuestion.requestId}: ${reason}`);
341
+ this.currentQuestion = null;
342
+ }
343
+ /**
344
+ * Start Jev evaluation
345
+ */
346
+ startEvaluation() {
347
+ if (!this.currentQuestion)
348
+ return;
349
+ if (this.currentQuestion.state !== "pending") {
350
+ logWarn("[PendingQuestion]", `Cannot start evaluation: state is ${this.currentQuestion.state}`);
351
+ return;
352
+ }
353
+ this.currentQuestion.state = "evaluating";
354
+ logWarn("[PendingQuestion]", `Started evaluation for question ${this.currentQuestion.requestId}`);
355
+ }
356
+ /**
357
+ * Get pending question by request ID
358
+ */
359
+ getQuestion(requestId) {
360
+ return this.questions.get(requestId);
361
+ }
362
+ /**
363
+ * Get all questions
364
+ */
365
+ getAllQuestions() {
366
+ return Array.from(this.questions.values());
367
+ }
368
+ /**
369
+ * Clean up old resolved questions
370
+ */
371
+ cleanup(maxAgeMs = 60 * 60 * 1000) {
372
+ const now = Date.now();
373
+ for (const [id, q] of this.questions.entries()) {
374
+ if (q.resolvedAt && (now - q.resolvedAt.getTime()) > maxAgeMs) {
375
+ this.questions.delete(id);
376
+ }
377
+ }
378
+ }
379
+ /**
380
+ * Check if question is answered
381
+ */
382
+ isResolved() {
383
+ return this.currentQuestion?.state === "resolved";
384
+ }
385
+ /**
386
+ * Get resolution
387
+ */
388
+ getResolution() {
389
+ return this.currentQuestion?.resolution;
390
+ }
391
+ /**
392
+ * Check if should suppress completion
393
+ */
394
+ shouldSuppressCompletion() {
395
+ return this.hasPendingQuestion();
396
+ }
397
+ }
398
+ /** Singleton instance */
399
+ let instance = null;
400
+ export function getPendingQuestionController() {
401
+ if (!instance) {
402
+ instance = new PendingQuestionController();
403
+ }
404
+ return instance;
405
+ }
406
+ export function createPendingQuestionController(config) {
407
+ instance = new PendingQuestionController(config);
408
+ return instance;
409
+ }
410
+ //# sourceMappingURL=pending-question.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-question.js","sourceRoot":"","sources":["../src/pending-question.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/notification",
3
- "version": "1.1.346",
3
+ "version": "1.1.348",
4
4
  "description": "pi-harness-runtime — notification",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,6 +8,7 @@ export * from "./notification-center.js";
8
8
  export * from "./base-adapter.js";
9
9
  export * from "./types.js";
10
10
  export * from "./telegram-webhook-handler.js";
11
+ export * from "./pending-question.js";
11
12
  export { requiresApproval, isHighPriority } from "./notification-policy.js";
12
13
  export { LineAdapter } from "./adapters/line-adapter.js";
13
14
  export { TelegramAdapter } from "./adapters/telegram-adapter.js";
@@ -10,6 +10,9 @@
10
10
  */
11
11
 
12
12
  import { logError, logWarn } from "./file-logger.js";
13
+ import { existsSync, mkdirSync } from "fs";
14
+ import { join } from "path";
15
+ import { DatabaseSync } from "node:sqlite";
13
16
 
14
17
  import type {
15
18
  InlineKeyboardButton,
@@ -45,9 +48,69 @@ export class NotificationCenter {
45
48
  private adapters: Map<string, ChannelAdapter> = new Map();
46
49
  private channelEnabled: Map<string, boolean> = new Map();
47
50
  private redactPatterns: RegExp[];
51
+ private db: DatabaseSync | null = null;
48
52
  /** Pending approval requests keyed by jobId */
49
53
  private pendingApprovals: Map<string, PendingApproval> = new Map();
50
54
 
55
+ /**
56
+ * Get SQLite database path
57
+ */
58
+ private static getDatabasePath(): string {
59
+ const home = process.env.HOME || process.env.USERPROFILE || "/home/frappe";
60
+ const dir = join(home, ".pi-harness-runtime");
61
+ if (!existsSync(dir)) {
62
+ mkdirSync(dir, { recursive: true });
63
+ }
64
+ return join(dir, "notification-settings.sqlite");
65
+ }
66
+
67
+ /**
68
+ * Initialize SQLite database
69
+ */
70
+ private initDatabase(): void {
71
+ try {
72
+ const dbPath = NotificationCenter.getDatabasePath();
73
+ this.db = new DatabaseSync(dbPath);
74
+
75
+ // Create tables
76
+ this.db.exec(`
77
+ CREATE TABLE IF NOT EXISTS channel_settings (
78
+ channel_id TEXT PRIMARY KEY,
79
+ enabled INTEGER NOT NULL DEFAULT 1,
80
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
81
+ );
82
+ `);
83
+
84
+ // Load existing settings
85
+ const stmt = this.db.prepare("SELECT channel_id, enabled FROM channel_settings");
86
+ const rows = stmt.all() as Array<{ channel_id: string; enabled: number }>;
87
+ for (const row of rows) {
88
+ this.channelEnabled.set(row.channel_id, row.enabled === 1);
89
+ }
90
+ logWarn("[NotificationCenter]", `Loaded ${rows.length} channel settings from SQLite`);
91
+ } catch (e) {
92
+ logError("[NotificationCenter]", `Failed to init database: ${e}`);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Save channel settings to SQLite
98
+ */
99
+ private saveChannelSettings(): void {
100
+ if (!this.db) return;
101
+ try {
102
+ const stmt = this.db.prepare(`
103
+ INSERT OR REPLACE INTO channel_settings (channel_id, enabled, updated_at)
104
+ VALUES (?, ?, CURRENT_TIMESTAMP)
105
+ `);
106
+ for (const [id, enabled] of this.channelEnabled.entries()) {
107
+ stmt.run(id, enabled ? 1 : 0);
108
+ }
109
+ } catch (e) {
110
+ logError("[NotificationCenter]", `Failed to save settings: ${e}`);
111
+ }
112
+ }
113
+
51
114
  /**
52
115
  * Track a new pending approval request
53
116
  */
@@ -116,10 +179,17 @@ export class NotificationCenter {
116
179
  this.redactPatterns =
117
180
  config?.redactPatterns ?? this.getDefaultRedactPatterns();
118
181
 
182
+ // Initialize SQLite database and load settings
183
+ this.initDatabase();
184
+
119
185
  if (config?.channels) {
120
186
  for (const channelConfig of config.channels) {
121
187
  if (channelConfig.enabled) {
122
188
  this.registerAdapter(channelConfig);
189
+ // Set default enabled state (will be overridden by loaded settings)
190
+ if (!this.channelEnabled.has(channelConfig.id)) {
191
+ this.channelEnabled.set(channelConfig.id, true);
192
+ }
123
193
  }
124
194
  }
125
195
  }
@@ -181,6 +251,7 @@ export class NotificationCenter {
181
251
  */
182
252
  setChannelEnabled(channelId: string, enabled: boolean): void {
183
253
  this.channelEnabled.set(channelId, enabled);
254
+ this.saveChannelSettings();
184
255
  logWarn("[NotificationCenter]", `Channel ${channelId} ${enabled ? "enabled" : "disabled"}`);
185
256
  }
186
257
 
@@ -424,7 +495,7 @@ export class NotificationCenter {
424
495
  // Track this pending approval so callback can resolve it
425
496
  this.trackPendingApproval(context.jobId, event, context);
426
497
 
427
- // Send to each channel with buttons (Telegram only)
498
+ // Send to each channel with buttons
428
499
  const results: NotificationResult[] = [];
429
500
 
430
501
  for (const [id, adapter] of this.adapters.entries()) {
@@ -440,6 +511,26 @@ export class NotificationCenter {
440
511
  } catch (error) {
441
512
  results.push({ success: false, channel: id, error: String(error) });
442
513
  }
514
+ } else if (adapter instanceof LineAdapter) {
515
+ // LINE: Send approval request with text fallback directing user to answer via Telegram or Pi
516
+ try {
517
+ const linePayload: NotificationPayload = {
518
+ jobId: context.jobId,
519
+ timestamp: new Date().toISOString(),
520
+ event,
521
+ title: "Approval Required",
522
+ message: context.requirement || "A decision is needed.",
523
+ details: {
524
+ jobId: context.jobId,
525
+ ...options?.additionalDetails,
526
+ },
527
+ };
528
+ const result = await adapter.send(linePayload);
529
+ results.push(result);
530
+ logWarn("[NotificationCenter]", `LINE approval sent to ${id}`);
531
+ } catch (error) {
532
+ results.push({ success: false, channel: id, error: String(error) });
533
+ }
443
534
  }
444
535
  }
445
536