swadhinbiswas feat: implement air-gapped mode, UI fixes, and project management integrations Β· 7/9/2026
e3bd4a1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
/**
* Notify CLI Commands
* Manage notifications and preferences
*/
import chalk from "chalk";
import Table from "cli-table3";
import { Command } from "commander";
import inquirer from "inquirer";
import ora from "ora";
import { getWithAuth, postWithAuth } from "../../lib/api.js";
import { getConfig } from "../../lib/config.js";
export const notifyCommand = new Command("notify").description(
"Manage notifications and preferences",
);
// List notifications
notifyCommand
.command("list")
.description("List your notifications")
.option("-u, --unread", "Show unread only")
.option("-n, --limit <n>", "Limit results", "20")
.option("--json", "Output as JSON")
.action(async (options) => {
const spinner = ora("Loading notifications...").start();
try {
const config = getConfig();
if (!config.token) {
spinner.fail("Not authenticated. Run 'och auth login' first.");
process.exit(1);
}
const filter = options.unread ? "unread" : "all";
const res = await getWithAuth(
`/api/notifications?filter=${filter}&limit=${options.limit}`,
);
const notifications = res.notifications || [];
spinner.stop();
if (options.json) {
console.log(JSON.stringify(notifications, null, 2));
return;
}
if (notifications.length === 0) {
console.log(chalk.hex("#6272a4")("\n π No notifications\n"));
return;
}
console.log(chalk.bold("\nπ Notifications\n"));
for (const notif of notifications) {
const icon = getNotificationIcon(notif.type);
const time = timeAgo(notif.createdAt);
const readIndicator = notif.isRead ? "" : chalk.hex("#8be9fd")("β");
console.log(
` ${icon} ${readIndicator} ${chalk.white(notif.title)} ${chalk.hex("#6272a4")(`(${time})`)}`,
);
if (notif.body) {
console.log(chalk.hex("#6272a4")(` ${notif.body.substring(0, 60)}...`));
}
}
console.log();
} catch (error: any) {
spinner.fail(`Failed to load notifications: ${error.message}`);
process.exit(1);
}
});
// Mark notification as read
notifyCommand
.command("read [id]")
.description("Mark notification(s) as read")
.option("-a, --all", "Mark all as read")
.action(async (id, options) => {
const spinner = ora("Marking as read...").start();
try {
if (options.all) {
await postWithAuth("/api/notifications/mark-all-read", {});
spinner.succeed("Marked all notifications as read");
} else if (id) {
await postWithAuth(`/api/notifications/${id}/read`, {});
spinner.succeed("Marked notification as read");
} else {
spinner.fail("Specify notification ID or use --all");
}
} catch (error: any) {
spinner.fail(`Failed: ${error.message}`);
}
});
// View/update notification settings
notifyCommand
.command("settings")
.description("View or update notification preferences")
.option("--show", "Show current settings")
.action(async (options) => {
try {
const config = getConfig();
if (!config.token) {
console.log(
chalk.hex("#ff5555")("Not authenticated. Run 'och auth login' first."),
);
process.exit(1);
}
// Get current settings
const spinner = ora("Loading settings...").start();
let currentSettings;
try {
currentSettings = await getWithAuth(
"/api/user/notification-preferences",
);
} catch {
currentSettings = { preferences: {}, quietHours: null };
}
spinner.stop();
if (options.show) {
console.log(chalk.bold("\nβοΈ Notification Settings\n"));
const eventTypes = [
"mention",
"assign",
"review_request",
"pr_approved",
"pr_merged",
"ci_passed",
"ci_failed",
];
const table = new Table({
head: [
chalk.hex("#6272a4")("Event"),
chalk.hex("#6272a4")("Email"),
chalk.hex("#6272a4")("In-App"),
],
});
for (const event of eventTypes) {
const pref = currentSettings.preferences?.[event] || {};
table.push([
event,
pref.emailEnabled ? chalk.hex("#50fa7b")("β") : chalk.hex("#ff5555")("β"),
pref.inAppEnabled ? chalk.hex("#50fa7b")("β") : chalk.hex("#ff5555")("β"),
]);
}
console.log(table.toString());
if (currentSettings.quietHours?.isEnabled) {
console.log(
chalk.hex("#f1fa8c")(
`\n π Quiet hours: ${currentSettings.quietHours.startTime} - ${currentSettings.quietHours.endTime}`,
),
);
}
console.log();
return;
}
// Interactive settings update
const { category } = await inquirer.prompt([
{
type: "list",
name: "category",
message: "What would you like to configure?",
choices: [
{ name: "Email notifications", value: "email" },
{ name: "Quiet hours", value: "quietHours" },
{ name: "Cancel", value: "cancel" },
],
},
]);
if (category === "cancel") return;
if (category === "quietHours") {
const hours = await inquirer.prompt([
{
type: "confirm",
name: "enabled",
message: "Enable quiet hours?",
default: currentSettings.quietHours?.isEnabled || false,
},
{
type: "input",
name: "start",
message: "Start time (HH:MM):",
default: currentSettings.quietHours?.startTime || "22:00",
when: (answers) => answers.enabled,
},
{
type: "input",
name: "end",
message: "End time (HH:MM):",
default: currentSettings.quietHours?.endTime || "08:00",
when: (answers) => answers.enabled,
},
]);
const saveSpinner = ora("Saving settings...").start();
await postWithAuth("/api/user/notification-preferences", {
quietHours: {
isEnabled: hours.enabled,
startTime: hours.start,
endTime: hours.end,
},
});
saveSpinner.succeed("Quiet hours updated");
}
} catch (error: any) {
console.error(chalk.hex("#ff5555")(`Failed: ${error.message}`));
}
});
function getNotificationIcon(type: string): string {
const icons: Record<string, string> = {
mention: "π¬",
assign: "π€",
review_request: "π",
pr_approved: "β
",
pr_changes_requested: "β οΈ",
pr_merged: "π",
ci_passed: "β
",
ci_failed: "β",
comment: "π¬",
};
return icons[type] || "π";
}
function timeAgo(date: string): string {
const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000);
if (seconds < 60) return "just now";
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
}
export default notifyCommand;