autonag 0.1.0
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/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1168 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +265 -0
- package/dist/index.js +156 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// cli.ts
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
+
import JSON5 from "json5";
|
|
8
|
+
|
|
9
|
+
// apiClient.ts
|
|
10
|
+
var ApiError = class extends Error {
|
|
11
|
+
constructor(status, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.name = "ApiError";
|
|
15
|
+
}
|
|
16
|
+
status;
|
|
17
|
+
};
|
|
18
|
+
function createApiClient(baseUrl, defaultToken) {
|
|
19
|
+
async function apiFetch(path, options = {}, token) {
|
|
20
|
+
const tok = token ?? defaultToken;
|
|
21
|
+
const headers = {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
...options.headers
|
|
24
|
+
};
|
|
25
|
+
if (tok) headers["Authorization"] = `Bearer ${tok}`;
|
|
26
|
+
const res = await fetch(`${baseUrl}${path}`, { ...options, headers });
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
let message = `HTTP ${res.status}`;
|
|
29
|
+
try {
|
|
30
|
+
const body = await res.json();
|
|
31
|
+
if (body.error) message = body.error;
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
throw new ApiError(res.status, message);
|
|
35
|
+
}
|
|
36
|
+
return res.json();
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
registerDevice: (nickname) => apiFetch(
|
|
40
|
+
"/devices/request",
|
|
41
|
+
{ method: "POST", body: JSON.stringify({ nickname }) }
|
|
42
|
+
),
|
|
43
|
+
getMe: (token) => apiFetch(
|
|
44
|
+
"/devices/me",
|
|
45
|
+
{},
|
|
46
|
+
token
|
|
47
|
+
),
|
|
48
|
+
requestInstance: (nickname, token) => apiFetch(
|
|
49
|
+
"/instances/request",
|
|
50
|
+
{ method: "POST", body: JSON.stringify({ nickname }) },
|
|
51
|
+
token
|
|
52
|
+
),
|
|
53
|
+
requestInstanceAccess: (instanceId, token, inviteCode) => apiFetch(
|
|
54
|
+
`/instances/${instanceId}/access/request`,
|
|
55
|
+
{ method: "POST", body: JSON.stringify(inviteCode ? { inviteCode } : {}) },
|
|
56
|
+
token
|
|
57
|
+
),
|
|
58
|
+
getInvite: (code) => apiFetch(
|
|
59
|
+
`/invites/${code}`
|
|
60
|
+
),
|
|
61
|
+
createInvite: (instanceId, token) => apiFetch(
|
|
62
|
+
`/instances/${instanceId}/invites`,
|
|
63
|
+
{ method: "POST", body: JSON.stringify({}) },
|
|
64
|
+
token
|
|
65
|
+
),
|
|
66
|
+
listInstances: (token) => apiFetch(
|
|
67
|
+
"/instances",
|
|
68
|
+
{},
|
|
69
|
+
token
|
|
70
|
+
),
|
|
71
|
+
addTimer: (instanceId, token, name, expiry, recurrence, description) => apiFetch(
|
|
72
|
+
`/instances/${instanceId}/timers`,
|
|
73
|
+
{ method: "POST", body: JSON.stringify({ name, expiry, ...description ? { description } : {}, ...recurrence ? { recurrence } : {} }) },
|
|
74
|
+
token
|
|
75
|
+
),
|
|
76
|
+
renameTimer: (instanceId, token, timerId, name) => apiFetch(
|
|
77
|
+
`/instances/${instanceId}/timers/${timerId}/name`,
|
|
78
|
+
{ method: "PATCH", body: JSON.stringify({ name }) },
|
|
79
|
+
token
|
|
80
|
+
),
|
|
81
|
+
updateExpiry: (instanceId, token, timerId, expiry) => apiFetch(
|
|
82
|
+
`/instances/${instanceId}/timers/${timerId}/expiry`,
|
|
83
|
+
{ method: "PATCH", body: JSON.stringify({ expiry }) },
|
|
84
|
+
token
|
|
85
|
+
),
|
|
86
|
+
completeTimer: (instanceId, token, timerId, closeReason) => apiFetch(
|
|
87
|
+
`/instances/${instanceId}/timers/${timerId}/complete`,
|
|
88
|
+
{ method: "POST", body: JSON.stringify({ closeReason }) },
|
|
89
|
+
token
|
|
90
|
+
),
|
|
91
|
+
removeTimer: (instanceId, token, timerId) => apiFetch(
|
|
92
|
+
`/instances/${instanceId}/timers/${timerId}`,
|
|
93
|
+
{ method: "DELETE" },
|
|
94
|
+
token
|
|
95
|
+
),
|
|
96
|
+
setShush: (instanceId, token, expiry) => apiFetch(
|
|
97
|
+
`/instances/${instanceId}/shush`,
|
|
98
|
+
{ method: "POST", body: JSON.stringify({ expiry }) },
|
|
99
|
+
token
|
|
100
|
+
),
|
|
101
|
+
clearShush: (instanceId, token) => apiFetch(
|
|
102
|
+
`/instances/${instanceId}/shush`,
|
|
103
|
+
{ method: "DELETE" },
|
|
104
|
+
token
|
|
105
|
+
),
|
|
106
|
+
listEvents: (instanceId, token, opts = {}) => {
|
|
107
|
+
const { from = 1, before, limit = 200 } = opts;
|
|
108
|
+
const params = before !== void 0 ? `before=${before}&limit=${limit}` : `from=${from}&limit=${limit}`;
|
|
109
|
+
return apiFetch(
|
|
110
|
+
`/instances/${instanceId}/events?${params}`,
|
|
111
|
+
{},
|
|
112
|
+
token
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
getTimerHistory: (instanceId, token, timerId, opts = {}) => {
|
|
116
|
+
const params = new URLSearchParams();
|
|
117
|
+
if (opts.after !== void 0) params.set("after", String(opts.after));
|
|
118
|
+
if (opts.limit !== void 0) params.set("limit", String(opts.limit));
|
|
119
|
+
if (opts.from !== void 0) params.set("from", String(opts.from));
|
|
120
|
+
if (opts.to !== void 0) params.set("to", String(opts.to));
|
|
121
|
+
const qs = params.toString();
|
|
122
|
+
return apiFetch(
|
|
123
|
+
`/instances/${instanceId}/timers/${timerId}/history${qs ? `?${qs}` : ""}`,
|
|
124
|
+
{},
|
|
125
|
+
token
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
skipEvent: (instanceId, token, eventId) => apiFetch(
|
|
129
|
+
`/instances/${instanceId}/events/${eventId}/skip`,
|
|
130
|
+
{ method: "POST" },
|
|
131
|
+
token
|
|
132
|
+
),
|
|
133
|
+
unskipEvent: (instanceId, token, eventId) => apiFetch(
|
|
134
|
+
`/instances/${instanceId}/events/${eventId}/skip`,
|
|
135
|
+
{ method: "DELETE" },
|
|
136
|
+
token
|
|
137
|
+
),
|
|
138
|
+
listComments: (instanceId, token, threadId) => apiFetch(
|
|
139
|
+
`/instances/${instanceId}/threads/${threadId}/comments`,
|
|
140
|
+
{},
|
|
141
|
+
token
|
|
142
|
+
),
|
|
143
|
+
addComment: (instanceId, token, threadId, text) => apiFetch(
|
|
144
|
+
`/instances/${instanceId}/threads/${threadId}/comments`,
|
|
145
|
+
{ method: "POST", body: JSON.stringify({ text }) },
|
|
146
|
+
token
|
|
147
|
+
),
|
|
148
|
+
getState: (instanceId, token) => apiFetch(
|
|
149
|
+
`/instances/${instanceId}/state`,
|
|
150
|
+
{},
|
|
151
|
+
token
|
|
152
|
+
),
|
|
153
|
+
batchActions: (instanceId, token, actions) => apiFetch(
|
|
154
|
+
`/instances/${instanceId}/actions`,
|
|
155
|
+
{ method: "POST", body: JSON.stringify({ actions }) },
|
|
156
|
+
token
|
|
157
|
+
)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// cli.ts
|
|
162
|
+
var CONFIG_PATH = join(homedir(), ".config", "autonag", "client.json5");
|
|
163
|
+
function loadConfig() {
|
|
164
|
+
if (!existsSync(CONFIG_PATH)) return { server: "http://localhost:3000" };
|
|
165
|
+
let raw;
|
|
166
|
+
try {
|
|
167
|
+
raw = readFileSync(CONFIG_PATH, "utf8");
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.error(`Cannot read config file ${CONFIG_PATH}: ${e}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
return JSON5.parse(raw);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.error(`Config file ${CONFIG_PATH} is not valid JSON5: ${e}`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function saveConfig(config) {
|
|
180
|
+
mkdirSync(join(CONFIG_PATH, ".."), { recursive: true });
|
|
181
|
+
writeFileSync(CONFIG_PATH, JSON5.stringify(config, null, 2));
|
|
182
|
+
}
|
|
183
|
+
function parseExpiry(input) {
|
|
184
|
+
if (input.startsWith("unix-ms:")) {
|
|
185
|
+
const n = Number(input.slice(8));
|
|
186
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
187
|
+
console.error(`Invalid expiry "${input}". Use unix-ms:<positive-integer>`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
return n;
|
|
191
|
+
}
|
|
192
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(input)) {
|
|
193
|
+
const ms = Date.parse(input);
|
|
194
|
+
if (isNaN(ms)) {
|
|
195
|
+
console.error(`Invalid datetime "${input}"`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
return ms;
|
|
199
|
+
}
|
|
200
|
+
const match = input.match(/^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);
|
|
201
|
+
if (match && (match[1] || match[2] || match[3] || match[4])) {
|
|
202
|
+
const d = parseInt(match[1] ?? "0");
|
|
203
|
+
const h = parseInt(match[2] ?? "0");
|
|
204
|
+
const m = parseInt(match[3] ?? "0");
|
|
205
|
+
const s = parseInt(match[4] ?? "0");
|
|
206
|
+
return Date.now() + (d * 86400 + h * 3600 + m * 60 + s) * 1e3;
|
|
207
|
+
}
|
|
208
|
+
console.error(`Cannot parse expiry "${input}". Use: 1d, 30m, 2h, 90s (relative duration), 2026-05-01T09:00:00Z (ISO 8601 with timezone), or unix-ms:<n>`);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
function fmtCountdown(ms) {
|
|
212
|
+
const sign = ms < 0 ? "-" : "";
|
|
213
|
+
const abs = Math.abs(ms);
|
|
214
|
+
const totalSec = Math.floor(abs / 1e3);
|
|
215
|
+
const d = Math.floor(totalSec / 86400);
|
|
216
|
+
const h = Math.floor(totalSec % 86400 / 3600);
|
|
217
|
+
const m = Math.floor(totalSec % 3600 / 60);
|
|
218
|
+
const s = totalSec % 60;
|
|
219
|
+
if (d > 0) return `${sign}${d}d ${h}h`;
|
|
220
|
+
if (h > 0) return `${sign}${h}h ${m}m`;
|
|
221
|
+
if (m > 0) return `${sign}${m}m ${s}s`;
|
|
222
|
+
return `${sign}${s}s`;
|
|
223
|
+
}
|
|
224
|
+
function fmtDate(ms) {
|
|
225
|
+
return new Date(ms).toLocaleString();
|
|
226
|
+
}
|
|
227
|
+
function shortId(id) {
|
|
228
|
+
return id.slice(0, 8);
|
|
229
|
+
}
|
|
230
|
+
function col(s, w) {
|
|
231
|
+
return s.slice(0, w).padEnd(w);
|
|
232
|
+
}
|
|
233
|
+
async function resolveTimerId(prefix, cfg2) {
|
|
234
|
+
if (/^[0-9a-f-]{36}$/.test(prefix)) return prefix;
|
|
235
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
236
|
+
const state = await api.getState(cfg2.instanceId, cfg2.token);
|
|
237
|
+
const matches = state.timers.filter((t) => t.id.startsWith(prefix));
|
|
238
|
+
if (matches.length === 0) {
|
|
239
|
+
console.error(`Timer not found: ${prefix}`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
if (matches.length > 1) {
|
|
243
|
+
console.error(`Ambiguous prefix "${prefix}" matches: ${matches.map((t) => shortId(t.id) + "\u2026").join(", ")}`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
return matches[0].id;
|
|
247
|
+
}
|
|
248
|
+
async function resolveEventId(prefix, cfg2) {
|
|
249
|
+
if (/^[0-9a-f-]{36}$/.test(prefix)) return prefix;
|
|
250
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
251
|
+
const allIds = [];
|
|
252
|
+
let from = 1;
|
|
253
|
+
while (true) {
|
|
254
|
+
const { events, latestSeq } = await api.listEvents(cfg2.instanceId, cfg2.token, { from, limit: 200 });
|
|
255
|
+
allIds.push(...events.map((e) => e.id));
|
|
256
|
+
if (events.length < 200 || events[events.length - 1].seq >= latestSeq) break;
|
|
257
|
+
from = events[events.length - 1].seq + 1;
|
|
258
|
+
}
|
|
259
|
+
const matches = allIds.filter((id) => id.startsWith(prefix));
|
|
260
|
+
if (matches.length === 0) {
|
|
261
|
+
console.error(`Event not found: ${prefix}`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
if (matches.length > 1) {
|
|
265
|
+
console.error(`Ambiguous prefix "${prefix}" matches: ${matches.map((id) => shortId(id) + "\u2026").join(", ")}`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
return matches[0];
|
|
269
|
+
}
|
|
270
|
+
async function resolveInstanceId(prefix, cfg2) {
|
|
271
|
+
if (/^[0-9a-f-]{36}$/.test(prefix)) return prefix;
|
|
272
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
273
|
+
const { instances } = await api.listInstances(cfg2.token);
|
|
274
|
+
const matches = instances.filter((i) => i.id.startsWith(prefix));
|
|
275
|
+
if (matches.length === 0) {
|
|
276
|
+
console.error(`Instance not found: ${prefix}`);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
if (matches.length > 1) {
|
|
280
|
+
console.error(`Ambiguous prefix "${prefix}" matches: ${matches.map((i) => `"${i.nickname}" (${shortId(i.id)}\u2026)`).join(", ")}`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
return matches[0].id;
|
|
284
|
+
}
|
|
285
|
+
function requireToken(cfg2) {
|
|
286
|
+
if (!cfg2.token) {
|
|
287
|
+
console.error("Not registered. Run: autonag register <nickname>");
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function requireInstance(cfg2) {
|
|
292
|
+
if (!cfg2.instanceId) {
|
|
293
|
+
console.error("No instance selected. Run: autonag instances select <id>");
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async function cmdRegister(nickname, cfg2) {
|
|
298
|
+
const api = createApiClient(cfg2.server);
|
|
299
|
+
const { device, token } = await api.registerDevice(nickname);
|
|
300
|
+
cfg2.token = token;
|
|
301
|
+
cfg2.deviceId = device.id;
|
|
302
|
+
saveConfig(cfg2);
|
|
303
|
+
console.log(`Registered as "${nickname}" (${shortId(device.id)}\u2026)`);
|
|
304
|
+
console.log(`Token saved to ${CONFIG_PATH}`);
|
|
305
|
+
console.log(`Waiting for admin approval \u2014 run "autonag status" to check.`);
|
|
306
|
+
}
|
|
307
|
+
async function cmdStatus(cfg2) {
|
|
308
|
+
console.log(`Server: ${cfg2.server}`);
|
|
309
|
+
console.log(`Config: ${CONFIG_PATH}`);
|
|
310
|
+
if (!cfg2.token) {
|
|
311
|
+
console.log(`Auth: not registered (run: autonag register <nickname>)`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
315
|
+
try {
|
|
316
|
+
const { device: d } = await api.getMe(cfg2.token);
|
|
317
|
+
const status = d.approvedAt ? `approved ${fmtDate(d.approvedAt)}` : "PENDING APPROVAL";
|
|
318
|
+
console.log(`Auth: ${d.nickname} (${shortId(d.id)}\u2026) [${status}]`);
|
|
319
|
+
} catch (e) {
|
|
320
|
+
if (e instanceof ApiError && e.status === 401) {
|
|
321
|
+
console.log(`Auth: token invalid or expired`);
|
|
322
|
+
} else throw e;
|
|
323
|
+
}
|
|
324
|
+
console.log(`Instance: ${cfg2.instanceId ?? "none selected (run: autonag instances select <id>)"}`);
|
|
325
|
+
}
|
|
326
|
+
async function cmdInstancesList(cfg2) {
|
|
327
|
+
requireToken(cfg2);
|
|
328
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
329
|
+
const { instances } = await api.listInstances(cfg2.token);
|
|
330
|
+
if (instances.length === 0) {
|
|
331
|
+
console.log("No accessible instances.");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
for (const i of instances) {
|
|
335
|
+
const status = i.approvedAt ? "active" : "PENDING";
|
|
336
|
+
const marker = i.id === cfg2.instanceId ? " *" : " ";
|
|
337
|
+
console.log(`${marker} ${shortId(i.id)}\u2026 "${i.nickname}" [${status}]`);
|
|
338
|
+
}
|
|
339
|
+
console.log(`
|
|
340
|
+
* = current default`);
|
|
341
|
+
}
|
|
342
|
+
async function cmdInstancesRequest(nickname, cfg2) {
|
|
343
|
+
requireToken(cfg2);
|
|
344
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
345
|
+
const { instance } = await api.requestInstance(nickname, cfg2.token);
|
|
346
|
+
console.log(`Requested instance "${instance.nickname}" (${shortId(instance.id)}\u2026)`);
|
|
347
|
+
console.log(`Waiting for admin approval.`);
|
|
348
|
+
}
|
|
349
|
+
async function cmdInstancesSelect(id, cfg2) {
|
|
350
|
+
requireToken(cfg2);
|
|
351
|
+
const instanceId = await resolveInstanceId(id, cfg2);
|
|
352
|
+
cfg2.instanceId = instanceId;
|
|
353
|
+
saveConfig(cfg2);
|
|
354
|
+
console.log(`Default instance set to ${instanceId}`);
|
|
355
|
+
}
|
|
356
|
+
async function cmdInstancesJoin(code, cfg2) {
|
|
357
|
+
requireToken(cfg2);
|
|
358
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
359
|
+
const { invite } = await api.getInvite(code);
|
|
360
|
+
if (!invite.valid) {
|
|
361
|
+
console.error(`Invite is not valid: ${invite.reason ?? "expired or already used"}`);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
await api.requestInstanceAccess(invite.instanceId, cfg2.token, code);
|
|
365
|
+
console.log(`Joined instance "${invite.instanceNickname}" (${shortId(invite.instanceId)}\u2026)`);
|
|
366
|
+
console.log(`Run: autonag instances select ${invite.instanceId}`);
|
|
367
|
+
}
|
|
368
|
+
async function cmdTimersList(cfg2) {
|
|
369
|
+
requireToken(cfg2);
|
|
370
|
+
requireInstance(cfg2);
|
|
371
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
372
|
+
const state = await api.getState(cfg2.instanceId, cfg2.token);
|
|
373
|
+
const now = Date.now();
|
|
374
|
+
const timers = [...state.timers].sort((a, b) => a.currentExpiry - b.currentExpiry);
|
|
375
|
+
if (timers.length === 0) {
|
|
376
|
+
console.log("No active timers.");
|
|
377
|
+
} else {
|
|
378
|
+
console.log(`${"ID".padEnd(10)} ${"Name".padEnd(24)} ${"Remaining".padEnd(16)} Recur`);
|
|
379
|
+
console.log("-".repeat(60));
|
|
380
|
+
for (const t of timers) {
|
|
381
|
+
const remaining = t.currentExpiry - now;
|
|
382
|
+
const label = remaining < 0 ? `OVERDUE ${fmtCountdown(remaining)}` : `in ${fmtCountdown(remaining)}`;
|
|
383
|
+
const recur = t.recurrence ? "\u21BB" : "";
|
|
384
|
+
console.log(`${col(shortId(t.id) + "\u2026", 10)} ${col(t.name, 24)} ${col(label, 16)} ${recur}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
console.log();
|
|
388
|
+
printAlarmStatus(state, now);
|
|
389
|
+
}
|
|
390
|
+
function printAlarmStatus(state, now) {
|
|
391
|
+
const { alarm, shush } = state;
|
|
392
|
+
const shushed = !!(shush && shush.expiry > now);
|
|
393
|
+
const colorLabel = {
|
|
394
|
+
blue: "off",
|
|
395
|
+
green: "green",
|
|
396
|
+
yellow: "yellow",
|
|
397
|
+
red: "red",
|
|
398
|
+
flashing: "FLASHING"
|
|
399
|
+
};
|
|
400
|
+
if (alarm.startsAt !== null) {
|
|
401
|
+
const diff = alarm.startsAt - now;
|
|
402
|
+
const colorStr = colorLabel[alarm.color] ?? alarm.color;
|
|
403
|
+
const timeStr = diff <= 0 ? `overdue ${fmtCountdown(diff)}` : `in ${fmtCountdown(diff)}`;
|
|
404
|
+
console.log(`[${colorStr}] "${alarm.urgentName}" \u2014 ${timeStr}`);
|
|
405
|
+
}
|
|
406
|
+
if (alarm.on) {
|
|
407
|
+
console.log(`Alarm: ringing.`);
|
|
408
|
+
} else if (shushed) {
|
|
409
|
+
const overdue = alarm.startsAt !== null && alarm.startsAt <= now;
|
|
410
|
+
const shushEndsAfterTimer = alarm.startsAt !== null && shush.expiry >= alarm.startsAt;
|
|
411
|
+
const necessary = overdue || shushEndsAfterTimer;
|
|
412
|
+
if (necessary) {
|
|
413
|
+
const ringIn = fmtCountdown(shush.expiry - now);
|
|
414
|
+
console.log(`Alarm: off. Will ring in ${ringIn}: Shush expires: ${alarm.urgentName}.`);
|
|
415
|
+
} else if (alarm.startsAt !== null) {
|
|
416
|
+
const ringIn = fmtCountdown(alarm.startsAt - now);
|
|
417
|
+
const remaining = fmtCountdown(shush.expiry - now);
|
|
418
|
+
console.log(`Alarm: off. Will ring in ${ringIn}: ${alarm.urgentName}. (Shushed unnecessarily for ${remaining}.)`);
|
|
419
|
+
} else {
|
|
420
|
+
const remaining = fmtCountdown(shush.expiry - now);
|
|
421
|
+
console.log(`Alarm: off. Shushed (unnecessarily) for ${remaining}.`);
|
|
422
|
+
}
|
|
423
|
+
} else if (alarm.startsAt !== null) {
|
|
424
|
+
const ringIn = fmtCountdown(alarm.startsAt - now);
|
|
425
|
+
console.log(`Alarm: off. Will ring in ${ringIn}: ${alarm.urgentName}.`);
|
|
426
|
+
} else {
|
|
427
|
+
console.log(`Alarm: off.`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function cmdAlarm(cfg2) {
|
|
431
|
+
requireToken(cfg2);
|
|
432
|
+
requireInstance(cfg2);
|
|
433
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
434
|
+
const state = await api.getState(cfg2.instanceId, cfg2.token);
|
|
435
|
+
printAlarmStatus(state, Date.now());
|
|
436
|
+
}
|
|
437
|
+
async function cmdTimersAdd(name, expiryStr, cfg2, opts) {
|
|
438
|
+
requireToken(cfg2);
|
|
439
|
+
requireInstance(cfg2);
|
|
440
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
441
|
+
const expiry = parseExpiry(expiryStr);
|
|
442
|
+
const recurrence = opts.days !== void 0 ? {
|
|
443
|
+
periodDays: opts.days,
|
|
444
|
+
anchorTime: opts.anchor ?? "09:00",
|
|
445
|
+
timezone: opts.tz ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
446
|
+
skipWeekends: opts.skipWeekends ?? false
|
|
447
|
+
} : void 0;
|
|
448
|
+
await api.addTimer(cfg2.instanceId, cfg2.token, name, expiry, recurrence, opts.desc);
|
|
449
|
+
const recurNote = recurrence ? ` (recurs every ${opts.days}d)` : "";
|
|
450
|
+
console.log(`Timer "${name}" added, expires ${fmtDate(expiry)}${recurNote}`);
|
|
451
|
+
}
|
|
452
|
+
async function cmdTimersRename(id, name, cfg2) {
|
|
453
|
+
requireToken(cfg2);
|
|
454
|
+
requireInstance(cfg2);
|
|
455
|
+
const timerId = await resolveTimerId(id, cfg2);
|
|
456
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
457
|
+
await api.renameTimer(cfg2.instanceId, cfg2.token, timerId, name);
|
|
458
|
+
console.log(`Timer ${shortId(timerId)}\u2026 renamed to "${name}"`);
|
|
459
|
+
}
|
|
460
|
+
async function cmdTimersSnooze(id, expiryStr, cfg2) {
|
|
461
|
+
requireToken(cfg2);
|
|
462
|
+
requireInstance(cfg2);
|
|
463
|
+
const timerId = await resolveTimerId(id, cfg2);
|
|
464
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
465
|
+
const expiry = parseExpiry(expiryStr);
|
|
466
|
+
await api.updateExpiry(cfg2.instanceId, cfg2.token, timerId, expiry);
|
|
467
|
+
console.log(`Timer ${shortId(timerId)}\u2026 snoozed until ${fmtDate(expiry)}`);
|
|
468
|
+
}
|
|
469
|
+
async function cmdTimersComplete(id, reason, cfg2) {
|
|
470
|
+
requireToken(cfg2);
|
|
471
|
+
requireInstance(cfg2);
|
|
472
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
473
|
+
const state = await api.getState(cfg2.instanceId, cfg2.token);
|
|
474
|
+
const timerId = await resolveTimerId(id, cfg2);
|
|
475
|
+
const timer = state.timers.find((t) => t.id === timerId);
|
|
476
|
+
if (timer?.recurrence) {
|
|
477
|
+
await api.batchActions(cfg2.instanceId, cfg2.token, [{ type: "recurTimer", timerId, closeReason: reason }]);
|
|
478
|
+
const newState = await api.getState(cfg2.instanceId, cfg2.token);
|
|
479
|
+
const updated = newState.timers.find((t) => t.id === timerId);
|
|
480
|
+
if (updated) {
|
|
481
|
+
console.log(`Timer "${timer.name}" recurred \u2014 next: ${fmtDate(updated.currentExpiry)}`);
|
|
482
|
+
} else {
|
|
483
|
+
console.log(`Timer ${shortId(timerId)}\u2026 recurred`);
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
await api.completeTimer(cfg2.instanceId, cfg2.token, timerId, reason);
|
|
487
|
+
console.log(`Timer ${shortId(timerId)}\u2026 completed (${reason})`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async function cmdTimersRemove(id, cfg2) {
|
|
491
|
+
requireToken(cfg2);
|
|
492
|
+
requireInstance(cfg2);
|
|
493
|
+
const timerId = await resolveTimerId(id, cfg2);
|
|
494
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
495
|
+
await api.removeTimer(cfg2.instanceId, cfg2.token, timerId);
|
|
496
|
+
console.log(`Timer ${shortId(timerId)}\u2026 removed`);
|
|
497
|
+
}
|
|
498
|
+
async function cmdTimersDescribe(id, text, cfg2) {
|
|
499
|
+
requireToken(cfg2);
|
|
500
|
+
requireInstance(cfg2);
|
|
501
|
+
const timerId = await resolveTimerId(id, cfg2);
|
|
502
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
503
|
+
await api.batchActions(cfg2.instanceId, cfg2.token, [{ type: "updateDescription", timerId, description: text }]);
|
|
504
|
+
console.log(`Timer ${shortId(timerId)}\u2026 description updated`);
|
|
505
|
+
}
|
|
506
|
+
async function cmdTimersRecurrenceSet(id, days, cfg2, opts) {
|
|
507
|
+
requireToken(cfg2);
|
|
508
|
+
requireInstance(cfg2);
|
|
509
|
+
const timerId = await resolveTimerId(id, cfg2);
|
|
510
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
511
|
+
const recurrence = {
|
|
512
|
+
periodDays: days,
|
|
513
|
+
anchorTime: opts.anchor ?? "09:00",
|
|
514
|
+
timezone: opts.tz ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
515
|
+
skipWeekends: opts.skipWeekends ?? false
|
|
516
|
+
};
|
|
517
|
+
await api.batchActions(cfg2.instanceId, cfg2.token, [{ type: "updateRecurrence", timerId, recurrence }]);
|
|
518
|
+
console.log(`Timer ${shortId(timerId)}\u2026 recurrence set to every ${days}d at ${recurrence.anchorTime} (${recurrence.timezone})`);
|
|
519
|
+
}
|
|
520
|
+
async function cmdTimersRecurrenceRemove(id, cfg2) {
|
|
521
|
+
requireToken(cfg2);
|
|
522
|
+
requireInstance(cfg2);
|
|
523
|
+
const timerId = await resolveTimerId(id, cfg2);
|
|
524
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
525
|
+
await api.batchActions(cfg2.instanceId, cfg2.token, [{ type: "removeRecurrence", timerId }]);
|
|
526
|
+
console.log(`Timer ${shortId(timerId)}\u2026 recurrence removed`);
|
|
527
|
+
}
|
|
528
|
+
async function cmdTimersHistory(id, cfg2) {
|
|
529
|
+
requireToken(cfg2);
|
|
530
|
+
requireInstance(cfg2);
|
|
531
|
+
const timerId = await resolveTimerId(id, cfg2);
|
|
532
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
533
|
+
const allEntries = [];
|
|
534
|
+
let cursor = 0;
|
|
535
|
+
while (cursor !== null) {
|
|
536
|
+
const { events, nextCursor } = await api.getTimerHistory(cfg2.instanceId, cfg2.token, timerId, { after: cursor });
|
|
537
|
+
for (const ev of events) {
|
|
538
|
+
const p = ev.payload;
|
|
539
|
+
const date = ev.type === "TimerRecurred" ? p["occurredAt"] ?? ev.timestamp : ev.timestamp;
|
|
540
|
+
allEntries.push({ date, closeReason: p["closeReason"] });
|
|
541
|
+
}
|
|
542
|
+
cursor = nextCursor;
|
|
543
|
+
}
|
|
544
|
+
if (allEntries.length === 0) {
|
|
545
|
+
console.log("No history yet.");
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
console.log(`${"Date".padEnd(22)} Reason`);
|
|
549
|
+
console.log("-".repeat(36));
|
|
550
|
+
for (const e of allEntries) {
|
|
551
|
+
console.log(`${col(fmtDate(e.date), 22)} ${e.closeReason}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async function cmdShush(durationStr, cfg2) {
|
|
555
|
+
requireToken(cfg2);
|
|
556
|
+
requireInstance(cfg2);
|
|
557
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
558
|
+
const expiry = durationStr ? parseExpiry(durationStr) : Date.now() + 30 * 60 * 1e3;
|
|
559
|
+
await api.setShush(cfg2.instanceId, cfg2.token, expiry);
|
|
560
|
+
console.log(`Alarm silenced until ${fmtDate(expiry)}`);
|
|
561
|
+
}
|
|
562
|
+
async function cmdUnshush(cfg2) {
|
|
563
|
+
requireToken(cfg2);
|
|
564
|
+
requireInstance(cfg2);
|
|
565
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
566
|
+
await api.clearShush(cfg2.instanceId, cfg2.token);
|
|
567
|
+
console.log(`Shush cleared`);
|
|
568
|
+
}
|
|
569
|
+
async function cmdEventsList(fromSeq, cfg2) {
|
|
570
|
+
requireToken(cfg2);
|
|
571
|
+
requireInstance(cfg2);
|
|
572
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
573
|
+
const { events } = await api.listEvents(cfg2.instanceId, cfg2.token, { from: fromSeq });
|
|
574
|
+
if (events.length === 0) {
|
|
575
|
+
console.log("No events.");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
console.log(`${"seq".padEnd(6)} ${"ID".padEnd(10)} ${"Type".padEnd(28)} ${"Time".padEnd(20)} Skipped`);
|
|
579
|
+
console.log("-".repeat(78));
|
|
580
|
+
for (const e of events) {
|
|
581
|
+
const skipped = e.skipped ? "yes" : "";
|
|
582
|
+
console.log(`${String(e.seq).padEnd(6)} ${col(shortId(e.id) + "\u2026", 10)} ${col(e.type, 28)} ${col(fmtDate(e.timestamp), 20)} ${skipped}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function cmdEventsSkip(id, cfg2) {
|
|
586
|
+
requireToken(cfg2);
|
|
587
|
+
requireInstance(cfg2);
|
|
588
|
+
const eventId = await resolveEventId(id, cfg2);
|
|
589
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
590
|
+
await api.skipEvent(cfg2.instanceId, cfg2.token, eventId);
|
|
591
|
+
console.log(`Event ${shortId(eventId)}\u2026 skipped`);
|
|
592
|
+
}
|
|
593
|
+
async function cmdEventsUnskip(id, cfg2) {
|
|
594
|
+
requireToken(cfg2);
|
|
595
|
+
requireInstance(cfg2);
|
|
596
|
+
const eventId = await resolveEventId(id, cfg2);
|
|
597
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
598
|
+
await api.unskipEvent(cfg2.instanceId, cfg2.token, eventId);
|
|
599
|
+
console.log(`Event ${shortId(eventId)}\u2026 unskipped`);
|
|
600
|
+
}
|
|
601
|
+
async function cmdCommentsList(id, cfg2) {
|
|
602
|
+
requireToken(cfg2);
|
|
603
|
+
requireInstance(cfg2);
|
|
604
|
+
const timerId = await resolveTimerId(id, cfg2);
|
|
605
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
606
|
+
const { comments } = await api.listComments(cfg2.instanceId, cfg2.token, timerId);
|
|
607
|
+
if (comments.length === 0) {
|
|
608
|
+
console.log("No comments.");
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
for (const c of comments) {
|
|
612
|
+
console.log(`[${fmtDate(c.createdAt)}] ${c.text}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
async function cmdCommentsAdd(id, text, cfg2) {
|
|
616
|
+
requireToken(cfg2);
|
|
617
|
+
requireInstance(cfg2);
|
|
618
|
+
const timerId = await resolveTimerId(id, cfg2);
|
|
619
|
+
const api = createApiClient(cfg2.server, cfg2.token);
|
|
620
|
+
await api.addComment(cfg2.instanceId, cfg2.token, timerId, text);
|
|
621
|
+
console.log(`Comment added`);
|
|
622
|
+
}
|
|
623
|
+
function cmdTutorial() {
|
|
624
|
+
console.log(`# autonag tutorial
|
|
625
|
+
|
|
626
|
+
autonag is a recurring-task tracker with an alarm. Timers count down; when
|
|
627
|
+
they expire, your alarm client starts ringing. You mark them done \u2014 if the
|
|
628
|
+
timer is recurring, the next occurrence is automatically scheduled.
|
|
629
|
+
|
|
630
|
+
## Config
|
|
631
|
+
|
|
632
|
+
Your config lives at \`~/.config/autonag/client.json5\`. It is a JSON5 file
|
|
633
|
+
(JSON with comments and trailing commas), so you can annotate it. After
|
|
634
|
+
registration it looks something like:
|
|
635
|
+
|
|
636
|
+
\`\`\`json5
|
|
637
|
+
{
|
|
638
|
+
server: 'https://your-server.example',
|
|
639
|
+
token: '\u2026',
|
|
640
|
+
deviceId: '\u2026',
|
|
641
|
+
instanceId: '\u2026',
|
|
642
|
+
}
|
|
643
|
+
\`\`\`
|
|
644
|
+
|
|
645
|
+
Edit it directly in a text editor whenever you need to change the server
|
|
646
|
+
URL or switch instances. To use a different config file:
|
|
647
|
+
|
|
648
|
+
\`\`\`
|
|
649
|
+
autonag --config /path/to/other.json5 timers list
|
|
650
|
+
\`\`\`
|
|
651
|
+
|
|
652
|
+
## First-time setup
|
|
653
|
+
|
|
654
|
+
Register this device \u2014 give it a short nickname so the admin knows who you are:
|
|
655
|
+
|
|
656
|
+
\`\`\`
|
|
657
|
+
autonag register myphone
|
|
658
|
+
\`\`\`
|
|
659
|
+
|
|
660
|
+
Your auth token and device ID are saved to the config. The device starts in
|
|
661
|
+
PENDING state until an admin approves it. Check your status:
|
|
662
|
+
|
|
663
|
+
\`\`\`
|
|
664
|
+
autonag status
|
|
665
|
+
\`\`\`
|
|
666
|
+
|
|
667
|
+
## Getting an instance
|
|
668
|
+
|
|
669
|
+
Timers live in an instance. Create a new one:
|
|
670
|
+
|
|
671
|
+
\`\`\`
|
|
672
|
+
autonag instances request home
|
|
673
|
+
\`\`\`
|
|
674
|
+
|
|
675
|
+
When the admin approves it, it shows up in your list:
|
|
676
|
+
|
|
677
|
+
\`\`\`
|
|
678
|
+
autonag instances list
|
|
679
|
+
\`\`\`
|
|
680
|
+
|
|
681
|
+
Set it as your default:
|
|
682
|
+
|
|
683
|
+
\`\`\`
|
|
684
|
+
autonag instances select <id>
|
|
685
|
+
\`\`\`
|
|
686
|
+
|
|
687
|
+
To join an instance someone else owns, ask them for an invite code. They
|
|
688
|
+
generate one with:
|
|
689
|
+
|
|
690
|
+
\`\`\`
|
|
691
|
+
autonag instances invite
|
|
692
|
+
\`\`\`
|
|
693
|
+
|
|
694
|
+
Then you join with the code they share:
|
|
695
|
+
|
|
696
|
+
\`\`\`
|
|
697
|
+
autonag instances join <code>
|
|
698
|
+
\`\`\`
|
|
699
|
+
|
|
700
|
+
Invite codes are single-use and expire after 7 days. The instance is not
|
|
701
|
+
discoverable by name \u2014 you need the code.
|
|
702
|
+
|
|
703
|
+
## Adding timers
|
|
704
|
+
|
|
705
|
+
One-shot reminder due in 2 days:
|
|
706
|
+
|
|
707
|
+
\`\`\`
|
|
708
|
+
autonag timers add "Doctor follow-up" 2d
|
|
709
|
+
\`\`\`
|
|
710
|
+
|
|
711
|
+
Recurring task every 7 days at 9 AM:
|
|
712
|
+
|
|
713
|
+
\`\`\`
|
|
714
|
+
autonag timers add "Weekly review" 7d --days 7 --anchor 09:00
|
|
715
|
+
\`\`\`
|
|
716
|
+
|
|
717
|
+
The first argument after the name is the initial expiry. \`--days\` makes it
|
|
718
|
+
recurring; \`--anchor\` sets the time of day; \`--tz\` overrides the timezone.
|
|
719
|
+
|
|
720
|
+
## Checking timers
|
|
721
|
+
|
|
722
|
+
\`\`\`
|
|
723
|
+
autonag timers list
|
|
724
|
+
\`\`\`
|
|
725
|
+
|
|
726
|
+
\`\`\`
|
|
727
|
+
ID Name Remaining Recur
|
|
728
|
+
------------------------------------------------------------
|
|
729
|
+
a1b2c3d4\u2026 Doctor follow-up in 1d 23h
|
|
730
|
+
e5f6a7b8\u2026 Weekly review in 6d 23h \u21BB
|
|
731
|
+
\`\`\`
|
|
732
|
+
|
|
733
|
+
The \u21BB symbol marks recurring timers.
|
|
734
|
+
|
|
735
|
+
## Completing a timer
|
|
736
|
+
|
|
737
|
+
Mark a timer done with a reason (pass, fail, or any custom string):
|
|
738
|
+
|
|
739
|
+
\`\`\`
|
|
740
|
+
autonag timers complete e5f6 pass
|
|
741
|
+
\`\`\`
|
|
742
|
+
|
|
743
|
+
For a one-shot timer this removes it. For a recurring timer it schedules
|
|
744
|
+
the next occurrence and tells you when:
|
|
745
|
+
|
|
746
|
+
\`\`\`
|
|
747
|
+
Timer "Weekly review" recurred \u2014 next: 4/24/2026, 9:00:00 AM
|
|
748
|
+
\`\`\`
|
|
749
|
+
|
|
750
|
+
To permanently stop a recurring timer, remove the recurrence first:
|
|
751
|
+
|
|
752
|
+
\`\`\`
|
|
753
|
+
autonag timers recurrence remove e5f6
|
|
754
|
+
autonag timers complete e5f6 done
|
|
755
|
+
\`\`\`
|
|
756
|
+
|
|
757
|
+
## Snoozing
|
|
758
|
+
|
|
759
|
+
Push a timer's deadline out:
|
|
760
|
+
|
|
761
|
+
\`\`\`
|
|
762
|
+
autonag timers snooze e5f6 3d
|
|
763
|
+
\`\`\`
|
|
764
|
+
|
|
765
|
+
## Silencing the alarm
|
|
766
|
+
|
|
767
|
+
Silence the alarm for 30 minutes (the default):
|
|
768
|
+
|
|
769
|
+
\`\`\`
|
|
770
|
+
autonag shush
|
|
771
|
+
\`\`\`
|
|
772
|
+
|
|
773
|
+
Or for a specific duration:
|
|
774
|
+
|
|
775
|
+
\`\`\`
|
|
776
|
+
autonag shush 2h
|
|
777
|
+
\`\`\`
|
|
778
|
+
|
|
779
|
+
Resume:
|
|
780
|
+
|
|
781
|
+
\`\`\`
|
|
782
|
+
autonag unshush
|
|
783
|
+
\`\`\`
|
|
784
|
+
|
|
785
|
+
## History
|
|
786
|
+
|
|
787
|
+
See every time a recurring timer was completed:
|
|
788
|
+
|
|
789
|
+
\`\`\`
|
|
790
|
+
autonag timers history e5f6
|
|
791
|
+
\`\`\`
|
|
792
|
+
|
|
793
|
+
## Notes and comments
|
|
794
|
+
|
|
795
|
+
Attach a description to a timer:
|
|
796
|
+
|
|
797
|
+
\`\`\`
|
|
798
|
+
autonag timers describe e5f6 "Prep the agenda before this"
|
|
799
|
+
\`\`\`
|
|
800
|
+
|
|
801
|
+
Add timestamped comments (good for logging what happened):
|
|
802
|
+
|
|
803
|
+
\`\`\`
|
|
804
|
+
autonag comments add e5f6 "Completed but flagged the follow-up"
|
|
805
|
+
\`\`\`
|
|
806
|
+
|
|
807
|
+
## Event log
|
|
808
|
+
|
|
809
|
+
Every action is recorded. View the log:
|
|
810
|
+
|
|
811
|
+
\`\`\`
|
|
812
|
+
autonag events list
|
|
813
|
+
\`\`\`
|
|
814
|
+
|
|
815
|
+
You can skip events to undo them \u2014 state is recomputed as if the
|
|
816
|
+
skipped event never happened:
|
|
817
|
+
|
|
818
|
+
\`\`\`
|
|
819
|
+
autonag events skip <event-id>
|
|
820
|
+
autonag events unskip <event-id>
|
|
821
|
+
\`\`\`
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
Run \`autonag --help\` for a full command reference.`);
|
|
825
|
+
}
|
|
826
|
+
var HELP = `autonag client CLI
|
|
827
|
+
|
|
828
|
+
Usage: autonag [options] <command> [args]
|
|
829
|
+
|
|
830
|
+
Options:
|
|
831
|
+
-h, --help Show this help
|
|
832
|
+
-s, --server <url> Server URL (default: http://localhost:3000)
|
|
833
|
+
-i, --instance <id> Instance ID (overrides saved default)
|
|
834
|
+
-c, --config <path> Config file (default: ~/.config/autonag/client.json5)
|
|
835
|
+
|
|
836
|
+
tutorial Walk through setup and daily workflow
|
|
837
|
+
|
|
838
|
+
Auth:
|
|
839
|
+
register <nickname> Register this device and save token
|
|
840
|
+
status Show auth status and saved config
|
|
841
|
+
|
|
842
|
+
Instances:
|
|
843
|
+
instances list
|
|
844
|
+
instances request <nickname> Create a new instance
|
|
845
|
+
instances join <code> Join an existing instance via invite code
|
|
846
|
+
instances invite Create an invite code for this instance
|
|
847
|
+
instances select <id>
|
|
848
|
+
|
|
849
|
+
Timers:
|
|
850
|
+
timers list
|
|
851
|
+
timers add <name> <expiry> [--desc <text>] [--days <n>] [--anchor <HH:MM>] [--tz <tz>] [--skip-weekends]
|
|
852
|
+
timers rename <id> <name>
|
|
853
|
+
timers snooze <id> <expiry> Update expiry: 1d, 30m, 2h, 90s | 2026-05-01T09:00:00Z | unix-ms:<n>
|
|
854
|
+
timers complete <id> <reason> Complete or recur (reason: pass, fail, or custom)
|
|
855
|
+
timers remove <id>
|
|
856
|
+
timers describe <id> <text> Set description
|
|
857
|
+
timers recurrence set <id> <days> [--anchor <HH:MM>] [--tz <tz>] [--skip-weekends]
|
|
858
|
+
timers recurrence remove <id>
|
|
859
|
+
timers history <id> Show completion history
|
|
860
|
+
|
|
861
|
+
Alarm:
|
|
862
|
+
alarm show Show current alarm state
|
|
863
|
+
|
|
864
|
+
Shush:
|
|
865
|
+
shush [duration] Silence alarm (default: 30m)
|
|
866
|
+
unshush Clear shush
|
|
867
|
+
|
|
868
|
+
Events:
|
|
869
|
+
events list [--from <seq>]
|
|
870
|
+
events skip <id>
|
|
871
|
+
events unskip <id>
|
|
872
|
+
|
|
873
|
+
Comments:
|
|
874
|
+
comments list <timerId>
|
|
875
|
+
comments add <timerId> <text>
|
|
876
|
+
|
|
877
|
+
Config saved to ~/.config/autonag/client.json5`;
|
|
878
|
+
function parseFlags(args, schema) {
|
|
879
|
+
const positional = [];
|
|
880
|
+
const flags = {};
|
|
881
|
+
for (let i = 0; i < args.length; i++) {
|
|
882
|
+
const a = args[i];
|
|
883
|
+
if (a.startsWith("--")) {
|
|
884
|
+
const key = a.slice(2);
|
|
885
|
+
if (!(key in schema)) {
|
|
886
|
+
console.error(`Unknown flag: --${key}`);
|
|
887
|
+
process.exit(1);
|
|
888
|
+
}
|
|
889
|
+
const type = schema[key];
|
|
890
|
+
if (type === "boolean") {
|
|
891
|
+
flags[key] = true;
|
|
892
|
+
} else {
|
|
893
|
+
const next = args[i + 1];
|
|
894
|
+
if (!next || next.startsWith("--")) {
|
|
895
|
+
console.error(`Flag --${key} requires a value`);
|
|
896
|
+
process.exit(1);
|
|
897
|
+
}
|
|
898
|
+
if (type === "int") {
|
|
899
|
+
const n = Number(next);
|
|
900
|
+
if (!Number.isInteger(n)) {
|
|
901
|
+
console.error(`Flag --${key} must be an integer, got: "${next}"`);
|
|
902
|
+
process.exit(1);
|
|
903
|
+
}
|
|
904
|
+
flags[key] = n;
|
|
905
|
+
} else {
|
|
906
|
+
flags[key] = next;
|
|
907
|
+
}
|
|
908
|
+
i++;
|
|
909
|
+
}
|
|
910
|
+
} else {
|
|
911
|
+
positional.push(a);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return { positional, flags };
|
|
915
|
+
}
|
|
916
|
+
var rawArgs = process.argv.slice(2);
|
|
917
|
+
var topArgs = [];
|
|
918
|
+
var serverOverride;
|
|
919
|
+
var instanceOverride;
|
|
920
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
921
|
+
const a = rawArgs[i];
|
|
922
|
+
if (a === "-h" || a === "--help") {
|
|
923
|
+
console.log(HELP);
|
|
924
|
+
process.exit(0);
|
|
925
|
+
} else if (a === "-s" || a === "--server") {
|
|
926
|
+
if (!rawArgs[i + 1] || rawArgs[i + 1].startsWith("-")) {
|
|
927
|
+
console.error("Flag -s/--server requires a URL");
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
930
|
+
serverOverride = rawArgs[++i];
|
|
931
|
+
} else if (a === "-i" || a === "--instance") {
|
|
932
|
+
if (!rawArgs[i + 1] || rawArgs[i + 1].startsWith("-")) {
|
|
933
|
+
console.error("Flag -i/--instance requires an ID");
|
|
934
|
+
process.exit(1);
|
|
935
|
+
}
|
|
936
|
+
instanceOverride = rawArgs[++i];
|
|
937
|
+
} else if (a === "-c" || a === "--config") {
|
|
938
|
+
if (!rawArgs[i + 1] || rawArgs[i + 1].startsWith("-")) {
|
|
939
|
+
console.error("Flag -c/--config requires a path");
|
|
940
|
+
process.exit(1);
|
|
941
|
+
}
|
|
942
|
+
CONFIG_PATH = rawArgs[++i];
|
|
943
|
+
} else topArgs.push(a);
|
|
944
|
+
}
|
|
945
|
+
var cfg = loadConfig();
|
|
946
|
+
if (serverOverride) cfg.server = serverOverride;
|
|
947
|
+
if (instanceOverride) cfg.instanceId = instanceOverride;
|
|
948
|
+
var [cmd, sub, ...rest] = topArgs;
|
|
949
|
+
try {
|
|
950
|
+
if (!cmd || cmd === "help") {
|
|
951
|
+
parseFlags(rest, {});
|
|
952
|
+
console.log(HELP);
|
|
953
|
+
} else if (cmd === "tutorial") {
|
|
954
|
+
parseFlags(rest, {});
|
|
955
|
+
cmdTutorial();
|
|
956
|
+
} else if (cmd === "register") {
|
|
957
|
+
parseFlags(rest, {});
|
|
958
|
+
if (!sub) {
|
|
959
|
+
console.error("Usage: autonag register <nickname>");
|
|
960
|
+
process.exit(1);
|
|
961
|
+
}
|
|
962
|
+
await cmdRegister(sub, cfg);
|
|
963
|
+
} else if (cmd === "status") {
|
|
964
|
+
parseFlags(rest, {});
|
|
965
|
+
await cmdStatus(cfg);
|
|
966
|
+
} else if (cmd === "instances") {
|
|
967
|
+
if (!sub || sub === "list") {
|
|
968
|
+
parseFlags(rest, {});
|
|
969
|
+
await cmdInstancesList(cfg);
|
|
970
|
+
} else if (sub === "request") {
|
|
971
|
+
const { positional } = parseFlags(rest, {});
|
|
972
|
+
if (!positional[0]) {
|
|
973
|
+
console.error("Usage: autonag instances request <nickname>");
|
|
974
|
+
process.exit(1);
|
|
975
|
+
}
|
|
976
|
+
await cmdInstancesRequest(positional[0], cfg);
|
|
977
|
+
} else if (sub === "join") {
|
|
978
|
+
const { positional } = parseFlags(rest, {});
|
|
979
|
+
if (!positional[0]) {
|
|
980
|
+
console.error("Usage: autonag instances join <code>");
|
|
981
|
+
process.exit(1);
|
|
982
|
+
}
|
|
983
|
+
await cmdInstancesJoin(positional[0], cfg);
|
|
984
|
+
} else if (sub === "invite") {
|
|
985
|
+
parseFlags(rest, {});
|
|
986
|
+
requireToken(cfg);
|
|
987
|
+
requireInstance(cfg);
|
|
988
|
+
const api = createApiClient(cfg.server, cfg.token);
|
|
989
|
+
const { invite } = await api.createInvite(cfg.instanceId, cfg.token);
|
|
990
|
+
console.log(`Invite code: ${invite.code}`);
|
|
991
|
+
console.log(`Expires: ${fmtDate(invite.expiresAt)}`);
|
|
992
|
+
console.log(`Share this code \u2014 it can only be used once.`);
|
|
993
|
+
} else if (sub === "select") {
|
|
994
|
+
const { positional } = parseFlags(rest, {});
|
|
995
|
+
if (!positional[0]) {
|
|
996
|
+
console.error("Usage: autonag instances select <id>");
|
|
997
|
+
process.exit(1);
|
|
998
|
+
}
|
|
999
|
+
await cmdInstancesSelect(positional[0], cfg);
|
|
1000
|
+
} else {
|
|
1001
|
+
console.error(`Unknown subcommand: instances ${sub}`);
|
|
1002
|
+
process.exit(1);
|
|
1003
|
+
}
|
|
1004
|
+
} else if (cmd === "timers") {
|
|
1005
|
+
if (!sub || sub === "list") {
|
|
1006
|
+
parseFlags(rest, {});
|
|
1007
|
+
await cmdTimersList(cfg);
|
|
1008
|
+
} else if (sub === "add") {
|
|
1009
|
+
const { positional, flags } = parseFlags(rest, {
|
|
1010
|
+
desc: "string",
|
|
1011
|
+
days: "int",
|
|
1012
|
+
anchor: "string",
|
|
1013
|
+
tz: "string",
|
|
1014
|
+
"skip-weekends": "boolean"
|
|
1015
|
+
});
|
|
1016
|
+
if (!positional[0] || !positional[1]) {
|
|
1017
|
+
console.error("Usage: autonag timers add <name> <expiry> [--desc <text>] [--days <n>] [--anchor <HH:MM>] [--tz <tz>] [--skip-weekends]");
|
|
1018
|
+
process.exit(1);
|
|
1019
|
+
}
|
|
1020
|
+
await cmdTimersAdd(positional[0], positional[1], cfg, {
|
|
1021
|
+
desc: flags.desc,
|
|
1022
|
+
days: flags.days,
|
|
1023
|
+
anchor: flags.anchor,
|
|
1024
|
+
tz: flags.tz,
|
|
1025
|
+
skipWeekends: flags["skip-weekends"] === true
|
|
1026
|
+
});
|
|
1027
|
+
} else if (sub === "rename") {
|
|
1028
|
+
const { positional } = parseFlags(rest, {});
|
|
1029
|
+
if (!positional[0] || !positional[1]) {
|
|
1030
|
+
console.error("Usage: autonag timers rename <id> <name>");
|
|
1031
|
+
process.exit(1);
|
|
1032
|
+
}
|
|
1033
|
+
await cmdTimersRename(positional[0], positional[1], cfg);
|
|
1034
|
+
} else if (sub === "snooze") {
|
|
1035
|
+
const { positional } = parseFlags(rest, {});
|
|
1036
|
+
if (!positional[0] || !positional[1]) {
|
|
1037
|
+
console.error("Usage: autonag timers snooze <id> <expiry>");
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
await cmdTimersSnooze(positional[0], positional[1], cfg);
|
|
1041
|
+
} else if (sub === "complete") {
|
|
1042
|
+
const { positional } = parseFlags(rest, {});
|
|
1043
|
+
if (!positional[0] || !positional[1]) {
|
|
1044
|
+
console.error("Usage: autonag timers complete <id> <reason>");
|
|
1045
|
+
process.exit(1);
|
|
1046
|
+
}
|
|
1047
|
+
await cmdTimersComplete(positional[0], positional[1], cfg);
|
|
1048
|
+
} else if (sub === "remove") {
|
|
1049
|
+
const { positional } = parseFlags(rest, {});
|
|
1050
|
+
if (!positional[0]) {
|
|
1051
|
+
console.error("Usage: autonag timers remove <id>");
|
|
1052
|
+
process.exit(1);
|
|
1053
|
+
}
|
|
1054
|
+
await cmdTimersRemove(positional[0], cfg);
|
|
1055
|
+
} else if (sub === "describe") {
|
|
1056
|
+
const { positional } = parseFlags(rest, {});
|
|
1057
|
+
if (!positional[0] || !positional[1]) {
|
|
1058
|
+
console.error("Usage: autonag timers describe <id> <text>");
|
|
1059
|
+
process.exit(1);
|
|
1060
|
+
}
|
|
1061
|
+
await cmdTimersDescribe(positional[0], positional[1], cfg);
|
|
1062
|
+
} else if (sub === "recurrence") {
|
|
1063
|
+
const recSub = rest[0];
|
|
1064
|
+
if (recSub === "set") {
|
|
1065
|
+
const { positional, flags } = parseFlags(rest, { anchor: "string", tz: "string", "skip-weekends": "boolean" });
|
|
1066
|
+
if (!positional[1] || !positional[2]) {
|
|
1067
|
+
console.error("Usage: autonag timers recurrence set <id> <days> [--anchor <HH:MM>] [--tz <tz>] [--skip-weekends]");
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
const days = Number(positional[2]);
|
|
1071
|
+
if (!Number.isInteger(days) || days <= 0) {
|
|
1072
|
+
console.error(`<days> must be a positive integer, got: "${positional[2]}"`);
|
|
1073
|
+
process.exit(1);
|
|
1074
|
+
}
|
|
1075
|
+
await cmdTimersRecurrenceSet(positional[1], days, cfg, {
|
|
1076
|
+
anchor: flags.anchor,
|
|
1077
|
+
tz: flags.tz,
|
|
1078
|
+
skipWeekends: flags["skip-weekends"] === true
|
|
1079
|
+
});
|
|
1080
|
+
} else if (recSub === "remove") {
|
|
1081
|
+
const { positional } = parseFlags(rest, {});
|
|
1082
|
+
if (!positional[1]) {
|
|
1083
|
+
console.error("Usage: autonag timers recurrence remove <id>");
|
|
1084
|
+
process.exit(1);
|
|
1085
|
+
}
|
|
1086
|
+
await cmdTimersRecurrenceRemove(positional[1], cfg);
|
|
1087
|
+
} else {
|
|
1088
|
+
console.error("Usage: autonag timers recurrence set|remove ...");
|
|
1089
|
+
process.exit(1);
|
|
1090
|
+
}
|
|
1091
|
+
} else if (sub === "history") {
|
|
1092
|
+
const { positional } = parseFlags(rest, {});
|
|
1093
|
+
if (!positional[0]) {
|
|
1094
|
+
console.error("Usage: autonag timers history <id>");
|
|
1095
|
+
process.exit(1);
|
|
1096
|
+
}
|
|
1097
|
+
await cmdTimersHistory(positional[0], cfg);
|
|
1098
|
+
} else {
|
|
1099
|
+
console.error(`Unknown subcommand: timers ${sub}`);
|
|
1100
|
+
process.exit(1);
|
|
1101
|
+
}
|
|
1102
|
+
} else if (cmd === "alarm") {
|
|
1103
|
+
if (sub !== "show") {
|
|
1104
|
+
console.error("Usage: autonag alarm show");
|
|
1105
|
+
process.exit(1);
|
|
1106
|
+
}
|
|
1107
|
+
parseFlags(rest, {});
|
|
1108
|
+
await cmdAlarm(cfg);
|
|
1109
|
+
} else if (cmd === "shush") {
|
|
1110
|
+
parseFlags(rest, {});
|
|
1111
|
+
await cmdShush(sub, cfg);
|
|
1112
|
+
} else if (cmd === "unshush") {
|
|
1113
|
+
parseFlags(rest, {});
|
|
1114
|
+
await cmdUnshush(cfg);
|
|
1115
|
+
} else if (cmd === "events") {
|
|
1116
|
+
if (!sub || sub === "list") {
|
|
1117
|
+
const { flags } = parseFlags(rest, { from: "int" });
|
|
1118
|
+
await cmdEventsList(flags.from ?? 1, cfg);
|
|
1119
|
+
} else if (sub === "skip") {
|
|
1120
|
+
const { positional } = parseFlags(rest, {});
|
|
1121
|
+
if (!positional[0]) {
|
|
1122
|
+
console.error("Usage: autonag events skip <id>");
|
|
1123
|
+
process.exit(1);
|
|
1124
|
+
}
|
|
1125
|
+
await cmdEventsSkip(positional[0], cfg);
|
|
1126
|
+
} else if (sub === "unskip") {
|
|
1127
|
+
const { positional } = parseFlags(rest, {});
|
|
1128
|
+
if (!positional[0]) {
|
|
1129
|
+
console.error("Usage: autonag events unskip <id>");
|
|
1130
|
+
process.exit(1);
|
|
1131
|
+
}
|
|
1132
|
+
await cmdEventsUnskip(positional[0], cfg);
|
|
1133
|
+
} else {
|
|
1134
|
+
console.error(`Unknown subcommand: events ${sub}`);
|
|
1135
|
+
process.exit(1);
|
|
1136
|
+
}
|
|
1137
|
+
} else if (cmd === "comments") {
|
|
1138
|
+
if (!sub || sub === "list") {
|
|
1139
|
+
const { positional } = parseFlags(rest, {});
|
|
1140
|
+
if (!positional[0]) {
|
|
1141
|
+
console.error("Usage: autonag comments list <timerId>");
|
|
1142
|
+
process.exit(1);
|
|
1143
|
+
}
|
|
1144
|
+
await cmdCommentsList(positional[0], cfg);
|
|
1145
|
+
} else if (sub === "add") {
|
|
1146
|
+
const { positional } = parseFlags(rest, {});
|
|
1147
|
+
if (!positional[0] || !positional[1]) {
|
|
1148
|
+
console.error("Usage: autonag comments add <timerId> <text>");
|
|
1149
|
+
process.exit(1);
|
|
1150
|
+
}
|
|
1151
|
+
await cmdCommentsAdd(positional[0], positional[1], cfg);
|
|
1152
|
+
} else {
|
|
1153
|
+
console.error(`Unknown subcommand: comments ${sub}`);
|
|
1154
|
+
process.exit(1);
|
|
1155
|
+
}
|
|
1156
|
+
} else {
|
|
1157
|
+
console.error(`Unknown command: ${cmd}`);
|
|
1158
|
+
console.error(`Run "autonag --help" for usage.`);
|
|
1159
|
+
process.exit(1);
|
|
1160
|
+
}
|
|
1161
|
+
} catch (e) {
|
|
1162
|
+
if (e instanceof ApiError) {
|
|
1163
|
+
console.error(`Error: ${e.message}`);
|
|
1164
|
+
process.exit(1);
|
|
1165
|
+
}
|
|
1166
|
+
throw e;
|
|
1167
|
+
}
|
|
1168
|
+
//# sourceMappingURL=cli.js.map
|