slack-term 1.23.1 → 1.25.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 +108 -7
- package/SKILL.md +76 -5
- package/dist/cli.js +92 -89
- package/package.json +3 -1
- package/ts/cache.ts +120 -0
- package/ts/cli.ts +586 -34
- package/ts/format.ts +13 -10
- package/ts/slack.ts +63 -2
- package/ts/tail.ts +11 -7
- package/ts/todo.ts +483 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slack-term",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/snomiao/slack-term.git"
|
|
@@ -65,6 +65,8 @@
|
|
|
65
65
|
"test:parity": "bash tests/parity.sh",
|
|
66
66
|
"record": "bun run tests/record.ts",
|
|
67
67
|
"anonymize": "bun run tests/anonymize.ts",
|
|
68
|
+
"scan-pii": "bash .githooks/pre-commit",
|
|
69
|
+
"prepare": "git config core.hooksPath .githooks 2>/dev/null || true",
|
|
68
70
|
"prepublishOnly": "bun run typecheck && bun run build"
|
|
69
71
|
},
|
|
70
72
|
"type": "module",
|
package/ts/cache.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Persistent, TTL'd lookup cache stored in ~/.config/slack-cli/cache.json.
|
|
2
|
+
//
|
|
3
|
+
// Scope is deliberately tiny — only two mappings that are cheap to re-derive and
|
|
4
|
+
// effectively immutable within an hour:
|
|
5
|
+
// channels: "#name" -> channel ID
|
|
6
|
+
// users: user ID -> handle
|
|
7
|
+
//
|
|
8
|
+
// Never cached (by design, see ts/todo.ts):
|
|
9
|
+
// - reaction state: it IS the mutable thing todo commands read to decide a
|
|
10
|
+
// no-op; a stale value would silently skip a real state change.
|
|
11
|
+
// - search results: Slack's search index already lags behind reactions.add,
|
|
12
|
+
// and caching on top of that lag turns "I set it but it isn't listed" into
|
|
13
|
+
// a much longer-lived bug.
|
|
14
|
+
//
|
|
15
|
+
// Every read/write is fail-soft: a corrupt JSON file, an unwritable directory or
|
|
16
|
+
// a permission error must never take a command down — we just run uncached.
|
|
17
|
+
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
|
|
22
|
+
export const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
23
|
+
|
|
24
|
+
export type CacheKind = "channels" | "users";
|
|
25
|
+
|
|
26
|
+
type Entry = { value: string; expires: number };
|
|
27
|
+
type CacheFile = { channels: Record<string, Entry>; users: Record<string, Entry> };
|
|
28
|
+
|
|
29
|
+
export const _internals = {
|
|
30
|
+
now: (): number => Date.now(),
|
|
31
|
+
// Same directory resolution as profiles.ts (~/.config/slack-cli/…).
|
|
32
|
+
path: (): string => join(process.env.HOME || homedir(), ".config", "slack-cli", "cache.json"),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let enabled = true;
|
|
36
|
+
let mem: CacheFile | undefined;
|
|
37
|
+
|
|
38
|
+
/** Disable persistence entirely (the --no-cache flag). Also drops anything loaded. */
|
|
39
|
+
export function setCacheEnabled(on: boolean): void {
|
|
40
|
+
enabled = on;
|
|
41
|
+
if (!on) mem = undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function empty(): CacheFile {
|
|
45
|
+
return { channels: {}, users: {} };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function load(): CacheFile {
|
|
49
|
+
if (mem) return mem;
|
|
50
|
+
mem = empty();
|
|
51
|
+
if (!enabled) return mem;
|
|
52
|
+
try {
|
|
53
|
+
const path = _internals.path();
|
|
54
|
+
if (!existsSync(path)) return mem;
|
|
55
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as Partial<CacheFile>;
|
|
56
|
+
// Tolerate any shape — a hand-edited or half-written file must not throw.
|
|
57
|
+
for (const kind of ["channels", "users"] as const) {
|
|
58
|
+
const section = raw?.[kind];
|
|
59
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) continue;
|
|
60
|
+
for (const [k, v] of Object.entries(section)) {
|
|
61
|
+
if (v && typeof v === "object" && typeof v.value === "string" && typeof v.expires === "number") {
|
|
62
|
+
mem[kind][k] = { value: v.value, expires: v.expires };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Corrupt/unreadable cache — start empty rather than fail the command.
|
|
68
|
+
mem = empty();
|
|
69
|
+
}
|
|
70
|
+
return mem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function persist(store: CacheFile): void {
|
|
74
|
+
if (!enabled) return;
|
|
75
|
+
try {
|
|
76
|
+
const path = _internals.path();
|
|
77
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
78
|
+
writeFileSync(path, JSON.stringify(store, null, 2) + "\n");
|
|
79
|
+
} catch {
|
|
80
|
+
// Read-only home, full disk, … — cache is an optimisation, never a hard dep.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Namespace every entry by workspace. Channel names AND user IDs are
|
|
85
|
+
* workspace-scoped: "#general" and "U123" mean different things in each team a
|
|
86
|
+
* token can reach, so an un-namespaced key would serve another workspace's
|
|
87
|
+
* channel ID for a full TTL — and `todo doctor --fix` writes reactions to
|
|
88
|
+
* whatever ID it is handed. An empty scope means "workspace unknown", and is
|
|
89
|
+
* treated as a permanent miss rather than risking a cross-workspace hit. */
|
|
90
|
+
function scopedKey(scope: string, key: string): string | undefined {
|
|
91
|
+
return scope ? `${scope}\t${key}` : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function cacheGet(kind: CacheKind, scope: string, key: string): string | undefined {
|
|
95
|
+
if (!enabled) return undefined;
|
|
96
|
+
const k = scopedKey(scope, key);
|
|
97
|
+
if (!k) return undefined;
|
|
98
|
+
const entry = load()[kind][k];
|
|
99
|
+
if (!entry) return undefined;
|
|
100
|
+
if (entry.expires <= _internals.now()) {
|
|
101
|
+
delete load()[kind][k];
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return entry.value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function cacheSet(kind: CacheKind, scope: string, key: string, value: string, ttlMs = CACHE_TTL_MS): void {
|
|
108
|
+
if (!enabled) return;
|
|
109
|
+
const k = scopedKey(scope, key);
|
|
110
|
+
if (!k) return;
|
|
111
|
+
const store = load();
|
|
112
|
+
store[kind][k] = { value, expires: _internals.now() + ttlMs };
|
|
113
|
+
persist(store);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Test hook — forget the in-process copy so the next read re-reads the file. */
|
|
117
|
+
export function resetCacheForTests(): void {
|
|
118
|
+
mem = undefined;
|
|
119
|
+
enabled = true;
|
|
120
|
+
}
|