slack-term 1.41.1 → 1.42.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/README.md +9 -0
- package/SKILL.md +12 -0
- package/dist/cli.js +210 -203
- package/package.json +1 -1
- package/ts/cli.ts +362 -112
- package/ts/sentlog.ts +106 -2
- package/ts/slack.ts +3 -2
package/ts/sentlog.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// Opt out with SLACK_TERM_ATTRIBUTION=off (no log, no metadata).
|
|
20
20
|
|
|
21
21
|
import { spawnSync } from "node:child_process";
|
|
22
|
-
import { chmodSync, mkdirSync } from "node:fs";
|
|
22
|
+
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
23
23
|
import { homedir, hostname } from "node:os";
|
|
24
24
|
import { basename, dirname, join } from "node:path";
|
|
25
25
|
|
|
@@ -63,7 +63,13 @@ export interface SentRow {
|
|
|
63
63
|
collected_at?: number | null;
|
|
64
64
|
answer?: string | null;
|
|
65
65
|
answer_exit?: number | null;
|
|
66
|
+
/** Set whenever `--wait`/`--waitFor` handed an answer to its caller — the
|
|
67
|
+
* decision (exit 0) or a free-text reply (exit 5) — with WHICH answer, and
|
|
68
|
+
* the `--after` cursor that delivery ended at. `ask --pending` skips an
|
|
69
|
+
* answer already delivered, and resumes from that cursor. */
|
|
66
70
|
delivered_at?: number | null;
|
|
71
|
+
delivered_answer?: string | null;
|
|
72
|
+
delivered_cursor?: string | null;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
export function attributionEnabled(): boolean {
|
|
@@ -231,7 +237,9 @@ CREATE TABLE IF NOT EXISTS sent (
|
|
|
231
237
|
collected_at INTEGER,
|
|
232
238
|
answer TEXT,
|
|
233
239
|
answer_exit INTEGER,
|
|
234
|
-
delivered_at INTEGER
|
|
240
|
+
delivered_at INTEGER,
|
|
241
|
+
delivered_answer TEXT,
|
|
242
|
+
delivered_cursor TEXT
|
|
235
243
|
);
|
|
236
244
|
CREATE INDEX IF NOT EXISTS sent_msg ON sent(channel, ts);
|
|
237
245
|
CREATE INDEX IF NOT EXISTS sent_at ON sent(sent_at);
|
|
@@ -252,6 +260,11 @@ export function openSentLog(): Db | undefined {
|
|
|
252
260
|
try { chmodSync(path, 0o600); } catch { /* best-effort */ }
|
|
253
261
|
try {
|
|
254
262
|
db.exec(SCHEMA);
|
|
263
|
+
// Columns added after the table first shipped. ALTER fails harmlessly when
|
|
264
|
+
// the column is already there.
|
|
265
|
+
for (const col of ["delivered_answer TEXT", "delivered_cursor TEXT"]) {
|
|
266
|
+
try { db.exec(`ALTER TABLE sent ADD COLUMN ${col}`); } catch { /* exists */ }
|
|
267
|
+
}
|
|
255
268
|
return db;
|
|
256
269
|
} catch {
|
|
257
270
|
try { db.close(); } catch { /* ignore */ }
|
|
@@ -354,3 +367,94 @@ export function parseSince(s: string, now = Date.now()): number | undefined {
|
|
|
354
367
|
const t = Date.parse(s);
|
|
355
368
|
return Number.isNaN(t) ? undefined : t;
|
|
356
369
|
}
|
|
370
|
+
|
|
371
|
+
// --- ask collection ----------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
/** Run one bookkeeping UPDATE against an EXISTING log. Never creates the file
|
|
374
|
+
* (no log means no row to update) and never throws. */
|
|
375
|
+
function updateLog(sql: string, ...params: unknown[]): void {
|
|
376
|
+
if (!attributionEnabled() || !existsSync(sentDbPath())) return;
|
|
377
|
+
const db = openSentLog();
|
|
378
|
+
if (!db) return;
|
|
379
|
+
try {
|
|
380
|
+
db.prepare(sql).run(...params);
|
|
381
|
+
} catch {
|
|
382
|
+
// bookkeeping only
|
|
383
|
+
} finally {
|
|
384
|
+
try { db.close(); } catch { /* ignore */ }
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** The caller of `--wait`/`--waitFor` got the DECISION (exit 0): the ask is
|
|
389
|
+
* collected and drops off `ask --pending`. A no-op when the ask is not in this
|
|
390
|
+
* machine's log (asked elsewhere). */
|
|
391
|
+
export function markAskCollected(channel: string, ts: string, answer: string, cursor: string): void {
|
|
392
|
+
const now = Date.now();
|
|
393
|
+
updateLog(
|
|
394
|
+
`UPDATE sent SET collected_at = COALESCE(collected_at, ?), answer = ?, answer_exit = 0,
|
|
395
|
+
delivered_at = ?, delivered_answer = ?, delivered_cursor = ?
|
|
396
|
+
WHERE kind = 'ask' AND channel = ? AND ts = ?`,
|
|
397
|
+
now, answer, now, answer, cursor, channel, ts,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** The caller got a free-text reply (exit 5) — delivered, but NOT a decision,
|
|
402
|
+
* so the ask stays uncollected: a pill pressed later is still the answer. */
|
|
403
|
+
export function markAskDelivered(channel: string, ts: string, answer: string, cursor: string): void {
|
|
404
|
+
updateLog(
|
|
405
|
+
`UPDATE sent SET delivered_at = ?, delivered_answer = ?, delivered_cursor = ?
|
|
406
|
+
WHERE kind = 'ask' AND channel = ? AND ts = ? AND collected_at IS NULL`,
|
|
407
|
+
Date.now(), answer, cursor, channel, ts,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Logged asks nobody has collected yet, oldest first. `undefined` when the
|
|
412
|
+
* log cannot be opened at all (no SQLite in this runtime). */
|
|
413
|
+
export function uncollectedAsks(sinceMs: number): SentRow[] | undefined {
|
|
414
|
+
if (!existsSync(sentDbPath())) return [];
|
|
415
|
+
const db = openSentLog();
|
|
416
|
+
if (!db) return undefined;
|
|
417
|
+
try {
|
|
418
|
+
return db.prepare(
|
|
419
|
+
`SELECT * FROM sent WHERE kind = 'ask' AND collected_at IS NULL AND sent_at >= ? ORDER BY sent_at ASC, id ASC`,
|
|
420
|
+
).all(sinceMs) as SentRow[];
|
|
421
|
+
} finally {
|
|
422
|
+
try { db.close(); } catch { /* ignore */ }
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Is the agent that sent this row still the process at that pid? A pid alone
|
|
427
|
+
* is not enough — pids are reused, and telling someone "the asker is still
|
|
428
|
+
* running" about whatever now holds the number would send them to the wrong
|
|
429
|
+
* agent. So the process must also (a) have started before the message was
|
|
430
|
+
* sent, and (b) carry the process name of the recorded CLI, when that CLI is
|
|
431
|
+
* one we know by name. */
|
|
432
|
+
export function senderAlive(row: Pick<SentRow, "agent_pid" | "pid" | "cli" | "sent_at">): boolean {
|
|
433
|
+
const pid = row.agent_pid ?? row.pid;
|
|
434
|
+
if (!pidAlive(pid)) return false;
|
|
435
|
+
try {
|
|
436
|
+
const r = spawnSync("ps", ["-o", "lstart=,comm=", "-p", String(pid)], { encoding: "utf8", timeout: 2000 });
|
|
437
|
+
const line = (r.stdout ?? "").trim();
|
|
438
|
+
// lstart is a fixed 24-char date ("Sun Sep 27 10:00:00 2026"), comm follows.
|
|
439
|
+
const started = Date.parse(line.slice(0, 24));
|
|
440
|
+
if (!Number.isNaN(started) && started > row.sent_at + 1000) return false;
|
|
441
|
+
const comm = basename(line.slice(24).trim());
|
|
442
|
+
const known = new Set(Object.values(AGENT_PROCESS_NAMES));
|
|
443
|
+
if (row.cli && known.has(row.cli) && comm && AGENT_PROCESS_NAMES[comm] !== row.cli) return false;
|
|
444
|
+
} catch {
|
|
445
|
+
// ps unavailable: fall back to the bare pid check
|
|
446
|
+
}
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Is this pid still running? `kill(pid, 0)` checks without signalling;
|
|
451
|
+
* EPERM means it exists but belongs to someone else — still alive. */
|
|
452
|
+
export function pidAlive(pid: number | null | undefined): boolean {
|
|
453
|
+
if (!pid || pid <= 0) return false;
|
|
454
|
+
try {
|
|
455
|
+
process.kill(pid, 0);
|
|
456
|
+
return true;
|
|
457
|
+
} catch (e: unknown) {
|
|
458
|
+
return (e as { code?: string }).code === "EPERM";
|
|
459
|
+
}
|
|
460
|
+
}
|
package/ts/slack.ts
CHANGED
|
@@ -852,8 +852,9 @@ function userNotFoundError(ref: string, found: UserSearch): Error {
|
|
|
852
852
|
|
|
853
853
|
/**
|
|
854
854
|
* Resolve `@name` (or a raw U…/W… id) to a user ID. Used when DMing as a bot:
|
|
855
|
-
* the
|
|
856
|
-
*
|
|
855
|
+
* the caller then opens the DM with the bot token. `send` runs the lookup on the
|
|
856
|
+
* *user* token; `ask`/`poll --as-bot` run it on the bot token (the bot scopes
|
|
857
|
+
* include users:read), so a dead user token cannot break a bot question.
|
|
857
858
|
*/
|
|
858
859
|
export async function resolveUserId(token: string, ref: string, cookie?: string): Promise<string> {
|
|
859
860
|
// `@U0123456789` — an id wearing the `@` that every other user target takes.
|