spearkit 0.3.0 → 0.4.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/.claude/skills/spearkit/SKILL.md +247 -0
- package/.claude/skills/spearkit/reference/cheatsheet.md +329 -0
- package/AGENTS.md +261 -0
- package/README.md +23 -3
- package/dist/index.cjs +599 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +524 -2
- package/dist/index.d.ts +524 -2
- package/dist/index.js +576 -19
- package/dist/index.js.map +1 -1
- package/docs/README.md +72 -0
- package/docs/api-reference.md +777 -0
- package/docs/auto-defer.md +74 -0
- package/docs/client.md +245 -0
- package/docs/collectors.md +65 -0
- package/docs/commands.md +203 -0
- package/docs/components.md +281 -0
- package/docs/context-menus.md +121 -0
- package/docs/context.md +293 -0
- package/docs/cooldown.md +125 -0
- package/docs/env.md +130 -0
- package/docs/errors.md +73 -0
- package/docs/events.md +152 -0
- package/docs/getting-started.md +147 -0
- package/docs/guards.md +146 -0
- package/docs/loading.md +144 -0
- package/docs/logging.md +195 -0
- package/docs/messages.md +35 -0
- package/docs/migration.md +160 -0
- package/docs/options.md +163 -0
- package/docs/permissions.md +68 -0
- package/docs/plugins.md +116 -0
- package/docs/prefix.md +234 -0
- package/docs/scheduler.md +111 -0
- package/docs/shutdown.md +42 -0
- package/docs/store.md +90 -0
- package/docs/usage.md +188 -0
- package/llms-full.txt +4619 -0
- package/llms.txt +127 -0
- package/package.json +9 -3
package/docs/shutdown.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Graceful shutdown
|
|
2
|
+
|
|
3
|
+
A `Ctrl-C` or a container stop sends your process a signal. If you don't handle
|
|
4
|
+
it, the process dies mid-flight — the gateway connection, scheduler timers, and
|
|
5
|
+
any open database handles are reaped abruptly. Graceful shutdown runs an optional
|
|
6
|
+
cleanup hook, calls `client.destroy()` (which also stops spearkit's scheduler),
|
|
7
|
+
then exits — with a hard timeout so a wedged shutdown can't hang forever.
|
|
8
|
+
|
|
9
|
+
## On a SpearClient
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
client.enableGracefulShutdown({
|
|
13
|
+
onShutdown: () => db.close(), // flush state before we exit
|
|
14
|
+
});
|
|
15
|
+
await client.start();
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Progress is logged through `client.logger`. The method returns a disposer that
|
|
19
|
+
removes the signal handlers (useful for tests / hot-reload).
|
|
20
|
+
|
|
21
|
+
## Standalone
|
|
22
|
+
|
|
23
|
+
`gracefulShutdown(client, options)` works with any object that has a `destroy()`
|
|
24
|
+
method:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { gracefulShutdown } from "spearkit";
|
|
28
|
+
|
|
29
|
+
gracefulShutdown(client, { onShutdown: () => db.close() });
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Options
|
|
33
|
+
|
|
34
|
+
| Field | Default | Meaning |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `signals` | `["SIGINT", "SIGTERM"]` | Signals to listen for. |
|
|
37
|
+
| `timeoutMs` | `10000` | Force-exit if shutdown exceeds this. |
|
|
38
|
+
| `onShutdown` | — | Runs before `destroy()`; receives the signal. |
|
|
39
|
+
| `exit` | `true` | Call `process.exit()` when done (set `false` in tests). |
|
|
40
|
+
| `logger` | — | `{ info?, error? }` progress logger. |
|
|
41
|
+
|
|
42
|
+
Shutdown runs **once** — repeated signals during teardown are ignored.
|
package/docs/store.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Key-value store & settings
|
|
2
|
+
|
|
3
|
+
Almost every community bot needs to remember *something* per guild — a custom
|
|
4
|
+
prefix, a mod-log channel, a welcome message — and reaches for a database on day
|
|
5
|
+
one. spearkit ships a dependency-free `KeyValueStore` interface with two
|
|
6
|
+
backends, plus a typed per-guild settings helper. Swap in Redis/SQL later by
|
|
7
|
+
implementing the same interface.
|
|
8
|
+
|
|
9
|
+
## Stores
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { JsonStore, MemoryStore } from "spearkit";
|
|
13
|
+
|
|
14
|
+
const dev = new MemoryStore(); // in-memory, great for tests
|
|
15
|
+
const prod = new JsonStore("data/db.json"); // durable JSON file
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Both implement `KeyValueStore`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
await store.set("key", { any: "json" });
|
|
22
|
+
await store.get<{ any: string }>("key"); // typed read, or undefined
|
|
23
|
+
await store.has("key");
|
|
24
|
+
await store.delete("key"); // → boolean (existed?)
|
|
25
|
+
await store.keys(); // → string[]
|
|
26
|
+
await store.clear();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`MemoryStore` deep-clones on read and write, so callers can't mutate stored
|
|
30
|
+
state. `JsonStore` serves reads from an in-memory cache and commits writes
|
|
31
|
+
atomically (temp file + rename) through a queue — a crash mid-write can't corrupt
|
|
32
|
+
the file, and concurrent writes don't interleave.
|
|
33
|
+
|
|
34
|
+
## Typed per-guild settings
|
|
35
|
+
|
|
36
|
+
`createSettings` wraps a store with defaults. `get` always returns a complete
|
|
37
|
+
object; `set` persists *only* the overrides, so widening `defaults` later is
|
|
38
|
+
safe.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { JsonStore, createSettings } from "spearkit";
|
|
42
|
+
|
|
43
|
+
const settings = createSettings({
|
|
44
|
+
store: new JsonStore("data/guilds.json"),
|
|
45
|
+
defaults: { prefix: "!", modLogChannelId: null as string | null },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const cfg = await settings.get(guildId); // { prefix, modLogChannelId }
|
|
49
|
+
await settings.set(guildId, { prefix: "?" }); // shallow-merged + persisted
|
|
50
|
+
await settings.reset(guildId); // back to defaults
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Pass `namespace` to keep several settings groups in one store:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const guilds = createSettings({ store, defaults: { prefix: "!" }, namespace: "guild" });
|
|
57
|
+
const users = createSettings({ store, defaults: { xp: 0 }, namespace: "user" });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Dynamic per-guild prefix
|
|
61
|
+
|
|
62
|
+
A stored prefix is only useful if prefix commands respect it. `prefix.dynamic`
|
|
63
|
+
resolves extra prefix(es) per message — combine it with `createSettings` for true
|
|
64
|
+
per-guild prefixes:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const client = new SpearClient({
|
|
68
|
+
prefix: {
|
|
69
|
+
dynamic: async (message) =>
|
|
70
|
+
message.guildId ? (await settings.get(message.guildId)).prefix : null,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The resolver runs on every candidate message, so keep it fast (cache or use the
|
|
76
|
+
in-memory `JsonStore` cache). Returned prefixes are tried *in addition* to any
|
|
77
|
+
static `prefix`. See [Prefix commands](./prefix.md) for the rest of the prefix
|
|
78
|
+
system.
|
|
79
|
+
|
|
80
|
+
## Namespacing a raw store
|
|
81
|
+
|
|
82
|
+
`namespaced(store, prefix)` returns a `KeyValueStore` whose keys are
|
|
83
|
+
transparently prefixed — handy for sharing one file across features:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { namespaced } from "spearkit";
|
|
87
|
+
|
|
88
|
+
const tags = namespaced(store, "tags");
|
|
89
|
+
await tags.set("hello", "world"); // stored under "tags:hello"
|
|
90
|
+
```
|
package/docs/usage.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Usage tracking
|
|
2
|
+
|
|
3
|
+
Usage tracking records **who used what**: every command, component, context-menu
|
|
4
|
+
and prefix-command invocation — successful or errored — becomes a `UsageEvent`
|
|
5
|
+
that spearkit can persist to a
|
|
6
|
+
store and/or mirror into a Discord channel. Turn it on with the client's `usage`
|
|
7
|
+
option.
|
|
8
|
+
|
|
9
|
+
## Usage tracking vs the logger
|
|
10
|
+
|
|
11
|
+
These look similar but answer different questions, and they are completely
|
|
12
|
+
independent sinks:
|
|
13
|
+
|
|
14
|
+
| | Logger | Usage tracking |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| Question | *What is the bot doing?* (diagnostics) | *Who used which feature?* (audit) |
|
|
17
|
+
| Content | Free-form messages, levels, errors, internals | Structured `UsageEvent`s for every completed use (with its `outcome`) |
|
|
18
|
+
| Sinks | Console / your log pipeline | A database store and/or a Discord channel |
|
|
19
|
+
| Configured by | the `logger` option | the `usage` option |
|
|
20
|
+
|
|
21
|
+
Both successes and handler errors are recorded as usage events — an error carries
|
|
22
|
+
`outcome: "error"` and an `errorMessage` — so usage is a complete audit trail.
|
|
23
|
+
The [logger](./logging.md) is for debugging; usage tracking is for analytics,
|
|
24
|
+
audit trails, and "top commands" dashboards.
|
|
25
|
+
|
|
26
|
+
## Enabling it
|
|
27
|
+
|
|
28
|
+
Set the `usage` option. Provide a `store` (a database), a `channel` (a Discord
|
|
29
|
+
channel id to mirror events into), or both:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { MemoryUsageStore, SpearClient } from "spearkit";
|
|
33
|
+
|
|
34
|
+
const client = new SpearClient({
|
|
35
|
+
usage: {
|
|
36
|
+
store: new MemoryUsageStore(),
|
|
37
|
+
channel: "123456789012345678", // optional: also post each event here
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Once enabled, spearkit auto-tracks every command, component, context-menu and
|
|
43
|
+
prefix-command invocation — successes and errors alike — with no tracking code in
|
|
44
|
+
your handlers.
|
|
45
|
+
|
|
46
|
+
## The usage event
|
|
47
|
+
|
|
48
|
+
Each tracked use is a `UsageEvent`:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
interface UsageEvent {
|
|
52
|
+
type: UsageType; // "command" | "prefix" | "component" | "event"
|
|
53
|
+
name: string; // command / component / event name
|
|
54
|
+
userId?: string;
|
|
55
|
+
userTag?: string;
|
|
56
|
+
guildId?: string | null;
|
|
57
|
+
channelId?: string | null;
|
|
58
|
+
detail?: string; // free-form extra detail
|
|
59
|
+
outcome?: UsageOutcome; // "success" | "error"
|
|
60
|
+
durationMs?: number; // handler wall-clock time
|
|
61
|
+
options?: Readonly<Record<string, UsageMetaValue>>; // snapshot of typed options
|
|
62
|
+
errorMessage?: string; // set when outcome === "error"
|
|
63
|
+
timestamp: Date;
|
|
64
|
+
}
|
|
65
|
+
type UsageType = "command" | "prefix" | "component" | "event";
|
|
66
|
+
type UsageOutcome = "success" | "error";
|
|
67
|
+
type UsageMetaValue = string | number | boolean | null;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Stores (the database)
|
|
71
|
+
|
|
72
|
+
A store is any object implementing `UsageStore`:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
interface UsageStore {
|
|
76
|
+
record(event: UsageEvent): void | Promise<void>;
|
|
77
|
+
all(): UsageEvent[] | Promise<readonly UsageEvent[]>;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
spearkit ships two.
|
|
82
|
+
|
|
83
|
+
### MemoryUsageStore
|
|
84
|
+
|
|
85
|
+
In-memory and synchronous — ideal for tests, prototypes, and live dashboards.
|
|
86
|
+
Pass an optional cap to keep only the most recent N events:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { MemoryUsageStore } from "spearkit";
|
|
90
|
+
|
|
91
|
+
const store = new MemoryUsageStore(1_000); // keep the last 1,000 events
|
|
92
|
+
|
|
93
|
+
store.all(); // readonly UsageEvent[]
|
|
94
|
+
store.size; // number of events held
|
|
95
|
+
store.byUser("123..."); // UsageEvent[] for one user id
|
|
96
|
+
store.clear(); // forget everything
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### JsonFileUsageStore
|
|
100
|
+
|
|
101
|
+
Durable and dependency-free: appends one event per line as newline-delimited
|
|
102
|
+
JSON (`.jsonl`). `all()` reads the file back and parses it, so it behaves like a
|
|
103
|
+
small file-backed database:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { JsonFileUsageStore } from "spearkit";
|
|
107
|
+
|
|
108
|
+
const store = new JsonFileUsageStore("./usage.jsonl");
|
|
109
|
+
|
|
110
|
+
await store.record({ type: "command", name: "ping", timestamp: new Date() });
|
|
111
|
+
const events = await store.all(); // readonly UsageEvent[]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The directory is created on demand. Because `all()` is async here (it reads from
|
|
115
|
+
disk), always `await` it.
|
|
116
|
+
|
|
117
|
+
## Querying the store
|
|
118
|
+
|
|
119
|
+
`client.usage.store` is the store you configured — query it directly. Note that
|
|
120
|
+
`all()` may be synchronous (`MemoryUsageStore`) or asynchronous
|
|
121
|
+
(`JsonFileUsageStore`); awaiting works for both:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const store = client.usage.store;
|
|
125
|
+
if (store !== undefined) {
|
|
126
|
+
const events = await store.all();
|
|
127
|
+
const topCommand = events.filter((e) => e.type === "command").length;
|
|
128
|
+
console.log(`${topCommand} command uses recorded`);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## The Discord channel reporter
|
|
133
|
+
|
|
134
|
+
Besides (or instead of) a store, you can mirror each event into a Discord
|
|
135
|
+
channel. Pass `channel` (a channel id) in the `usage` option; spearkit posts one
|
|
136
|
+
line per event using `formatUsage`:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { SpearClient } from "spearkit";
|
|
140
|
+
|
|
141
|
+
new SpearClient({ usage: { channel: "123456789012345678" } });
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`formatUsage(event)` is the default renderer (e.g. `` `command` **ping** by
|
|
145
|
+
user#0001 in <#…> ``). Override it with `format` to control the line:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { SpearClient, type UsageEvent } from "spearkit";
|
|
149
|
+
|
|
150
|
+
new SpearClient({
|
|
151
|
+
usage: {
|
|
152
|
+
channel: "123456789012345678",
|
|
153
|
+
format: (event: UsageEvent) => `${event.userTag ?? "someone"} used ${event.name}`,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## The usage tracker
|
|
159
|
+
|
|
160
|
+
`client.usage` is a `UsageTracker`. The client configures it from the `usage`
|
|
161
|
+
option, but you can drive it directly:
|
|
162
|
+
|
|
163
|
+
| Member | Description |
|
|
164
|
+
| ------ | ----------- |
|
|
165
|
+
| `setStore(store)` | Set (or swap) the persistence store. |
|
|
166
|
+
| `reportTo(channelId, format?)` | Mirror events into a channel, optionally with a custom formatter. |
|
|
167
|
+
| `track(event)` | Record a use. Returns immediately; storing/reporting run in the background. |
|
|
168
|
+
| `store` | The configured store, for querying. |
|
|
169
|
+
| `enabled` | `true` if a store or channel is configured. |
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { JsonFileUsageStore } from "spearkit";
|
|
173
|
+
|
|
174
|
+
client.usage.setStore(new JsonFileUsageStore("./usage.jsonl"));
|
|
175
|
+
client.usage.reportTo("123456789012345678");
|
|
176
|
+
|
|
177
|
+
// Record a custom event yourself (e.g. a non-command action).
|
|
178
|
+
client.usage.track({ type: "event", name: "signup", timestamp: new Date() });
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Tracking is fire-and-forget: a slow store or channel never blocks command
|
|
182
|
+
handling, and any failure is logged rather than thrown.
|
|
183
|
+
|
|
184
|
+
## See also
|
|
185
|
+
|
|
186
|
+
- [Logging](./logging.md) — diagnostics, the other sink.
|
|
187
|
+
- [Commands](./commands.md) / [Prefix commands](./prefix.md) / [Components](./components.md) — what gets tracked.
|
|
188
|
+
- [Client](./client.md) — the `usage` option.
|