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/guards.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Guards
|
|
2
|
+
|
|
3
|
+
Guards are declarative **preconditions** that run before a handler. They work
|
|
4
|
+
uniformly across slash commands, components (buttons, selects, modals), prefix
|
|
5
|
+
commands and context-menu commands — and can also be applied client-wide. A
|
|
6
|
+
guard returns `true` to allow the handler, or a denial (with an optional reason)
|
|
7
|
+
to block it; spearkit replies with the reason and the handler never runs.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { command, requireUserPermissions, PermissionFlagsBits } from "spearkit";
|
|
11
|
+
|
|
12
|
+
export const purge = command({
|
|
13
|
+
name: "purge",
|
|
14
|
+
description: "Bulk-delete messages",
|
|
15
|
+
guards: [requireUserPermissions(PermissionFlagsBits.ManageMessages)],
|
|
16
|
+
run: (ctx) => ctx.reply("Purged."),
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Where guards attach
|
|
21
|
+
|
|
22
|
+
Pass `guards: [...]` to any handler definition, or set client-wide defaults that
|
|
23
|
+
run before every handler's own guards.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import {
|
|
27
|
+
SpearClient,
|
|
28
|
+
command,
|
|
29
|
+
button,
|
|
30
|
+
prefixCommand,
|
|
31
|
+
userCommand,
|
|
32
|
+
guildOnly,
|
|
33
|
+
} from "spearkit";
|
|
34
|
+
|
|
35
|
+
// Per-handler — on commands, components, prefix and context-menu commands.
|
|
36
|
+
command({ name: "kick", description: "…", guards: [guildOnly()], run: () => {} });
|
|
37
|
+
button({ id: "del:{id}", guards: [guildOnly()], run: () => {} });
|
|
38
|
+
prefixCommand({ name: "ban", guards: [guildOnly()], run: () => {} });
|
|
39
|
+
userCommand({ name: "Report", guards: [guildOnly()], run: () => {} });
|
|
40
|
+
|
|
41
|
+
// Client-wide — applied before each handler's own guards.
|
|
42
|
+
const client = new SpearClient({ guards: [guildOnly()] });
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Client-wide guards run first; if they pass, the handler's own guards run next.
|
|
46
|
+
The first denial short-circuits the rest.
|
|
47
|
+
|
|
48
|
+
## Built-in guards
|
|
49
|
+
|
|
50
|
+
Each built-in returns a `Guard` and accepts an optional custom `reason`. When
|
|
51
|
+
omitted, a sensible default message is used (shown below).
|
|
52
|
+
|
|
53
|
+
| Guard | Denies unless… | Default reason |
|
|
54
|
+
| ----- | -------------- | -------------- |
|
|
55
|
+
| `guildOnly(reason?)` | used inside a guild | `"This can only be used in a server."` |
|
|
56
|
+
| `dmOnly(reason?)` | used in a DM | `"This can only be used in DMs."` |
|
|
57
|
+
| `requireAnyRole(roleIds, reason?)` | the member holds **any** of `roleIds` | `"You don't have permission to use this."` |
|
|
58
|
+
| `requireAllRoles(roleIds, reason?)` | the member holds **every** id in `roleIds` | `"You're missing one of the required roles."` |
|
|
59
|
+
| `requireOwner(ownerIds, reason?)` | the user id is in `ownerIds` | `"This is owner-only."` |
|
|
60
|
+
| `requireUserPermissions(permission, reason?)` | the member has the Discord `permission` | `"You don't have permission to use this."` |
|
|
61
|
+
| `requireBotPermissions(permission, reason?)` | the bot's member has the Discord `permission` | `"I don't have permission to do that here."` |
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import {
|
|
65
|
+
command,
|
|
66
|
+
requireAnyRole,
|
|
67
|
+
requireBotPermissions,
|
|
68
|
+
PermissionFlagsBits,
|
|
69
|
+
} from "spearkit";
|
|
70
|
+
|
|
71
|
+
export const announce = command({
|
|
72
|
+
name: "announce",
|
|
73
|
+
description: "Post an announcement",
|
|
74
|
+
guards: [
|
|
75
|
+
requireAnyRole(["111111111111111111"], "Staff only."),
|
|
76
|
+
requireBotPermissions(PermissionFlagsBits.SendMessages),
|
|
77
|
+
],
|
|
78
|
+
run: (ctx) => ctx.reply("Announced."),
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Custom guards
|
|
83
|
+
|
|
84
|
+
`guard(predicate)` wraps an inline predicate so a one-off check still types as a
|
|
85
|
+
`Guard`. The predicate receives a `GuardContext` and returns a `GuardResult`;
|
|
86
|
+
use `denied(reason?)` to build a denial.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { command, guard, denied } from "spearkit";
|
|
90
|
+
|
|
91
|
+
const cooldownOver = guard((ctx) =>
|
|
92
|
+
isReady(ctx.user.id) ? true : denied("Still warming up — try again soon."),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
export const cast = command({
|
|
96
|
+
name: "cast",
|
|
97
|
+
description: "Cast a spell",
|
|
98
|
+
guards: [cooldownOver],
|
|
99
|
+
run: (ctx) => ctx.reply("✨"),
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`GuardContext` exposes the actor/location fields every handler shares, so the
|
|
104
|
+
same guard works on commands, components, prefix and context-menu handlers:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
interface GuardContext {
|
|
108
|
+
client: Client;
|
|
109
|
+
user: User;
|
|
110
|
+
member: GuildMember | APIInteractionGuildMember | null;
|
|
111
|
+
guild: Guild | null;
|
|
112
|
+
guildId: string | null;
|
|
113
|
+
channelId: string | null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
type GuardResult = boolean | { allowed: false; reason?: string };
|
|
117
|
+
type Guard<TCtx extends GuardContext = GuardContext> = (ctx: TCtx) => Awaitable<GuardResult>;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Running guards manually
|
|
121
|
+
|
|
122
|
+
`runGuards(ctx, guards)` evaluates a list in order and short-circuits on the
|
|
123
|
+
first denial — useful if you build your own dispatch on top of spearkit.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { runGuards, guildOnly } from "spearkit";
|
|
127
|
+
|
|
128
|
+
const result = await runGuards(ctx, [guildOnly()]);
|
|
129
|
+
if (!result.allowed) {
|
|
130
|
+
// result.reason is the denial message (or undefined)
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`runGuards` resolves to `RunGuardsResult`:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
type RunGuardsResult = { allowed: true } | { allowed: false; reason: string | undefined };
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## See also
|
|
141
|
+
|
|
142
|
+
- [Commands](./commands.md) — `guards` on slash commands.
|
|
143
|
+
- [Components](./components.md) — `guards` on buttons, selects and modals.
|
|
144
|
+
- [Prefix commands](./prefix.md) — `guards` on text commands.
|
|
145
|
+
- [Context menus](./context-menus.md) — `guards` on "Apps" actions.
|
|
146
|
+
- [Cooldowns](./cooldown.md) — the other built-in precondition.
|
package/docs/loading.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# File-based loading
|
|
2
|
+
|
|
3
|
+
Instead of importing and registering every handler by hand, you can keep one
|
|
4
|
+
handler per file and let spearkit discover them. The loader imports a directory,
|
|
5
|
+
inspects each module's exports, and registers everything that is a command,
|
|
6
|
+
event, component, scheduled task or prefix command.
|
|
7
|
+
|
|
8
|
+
## `client.load`
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
load(dir: string, options?: LoadOptions): Promise<number>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`client.load` imports `dir` and registers every spearkit-registrable export it finds,
|
|
15
|
+
resolving to the number of items registered.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { SpearClient, Intents } from "spearkit";
|
|
20
|
+
|
|
21
|
+
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
22
|
+
|
|
23
|
+
const client = new SpearClient({ intents: Intents.default });
|
|
24
|
+
|
|
25
|
+
const loaded =
|
|
26
|
+
(await client.load(`${here}commands`)) +
|
|
27
|
+
(await client.load(`${here}events`)) +
|
|
28
|
+
(await client.load(`${here}components`));
|
|
29
|
+
console.log(`Loaded ${loaded} modules.`);
|
|
30
|
+
|
|
31
|
+
await client.start(process.env.DISCORD_TOKEN);
|
|
32
|
+
await client.deployCommands({ guildId: process.env.GUILD_ID });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### What gets registered
|
|
36
|
+
|
|
37
|
+
For every imported file, spearkit walks **all** of its exports — default *and*
|
|
38
|
+
named — and registers each value that is a command (`command`, `commandGroup`),
|
|
39
|
+
an event (`event`), a component (`button`, `stringSelect`, `modal`, …), a
|
|
40
|
+
scheduled task (`task`) or a prefix command (`prefixCommand`). Other exports
|
|
41
|
+
(helpers, constants, types) are ignored, and context-menu commands are **not**
|
|
42
|
+
auto-detected — register those explicitly. So both of these are picked up:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// default export
|
|
46
|
+
export default command({ name: "ping", description: "…", run: (ctx) => ctx.reply("pong") });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// named export
|
|
51
|
+
export const vote = button({ id: "vote:{choice}", label: "Vote", run: (ctx) => ctx.update(ctx.params.choice) });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Options
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
interface LoadOptions {
|
|
58
|
+
extensions?: readonly string[]; // default: [".js", ".mjs", ".cjs"]
|
|
59
|
+
recursive?: boolean; // default: true
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- **`extensions`** — which file extensions to import. By default the loader reads
|
|
64
|
+
`.js`, `.mjs` and `.cjs` — i.e. **compiled JavaScript**, not `.ts` source.
|
|
65
|
+
- **`recursive`** — by default the loader descends into subdirectories. Pass
|
|
66
|
+
`recursive: false` to load only the top level.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
await client.load(`${here}features`, { recursive: false });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
> The loader imports compiled JavaScript. **Build your TypeScript first**, then run
|
|
73
|
+
> (and load) the emitted output — `npx tsc && node dist/index.js`. Loading a
|
|
74
|
+
> directory of `.ts` source files will not match the default extensions.
|
|
75
|
+
|
|
76
|
+
## Standalone helpers
|
|
77
|
+
|
|
78
|
+
`client.load` is the method form of `loadInto`. Both helpers are exported if you
|
|
79
|
+
want to collect or register modules separately.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
collectModules(dir: string, options?: LoadOptions): Promise<Registerable[]>
|
|
83
|
+
loadInto(client: SpearClient, dir: string, options?: LoadOptions): Promise<number>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- **`collectModules`** imports a directory and returns the registrable exports it
|
|
87
|
+
found, without touching any client. Use it to inspect, filter, or combine
|
|
88
|
+
modules before registering.
|
|
89
|
+
- **`loadInto`** calls `collectModules` and then `client.register(...)` for you,
|
|
90
|
+
returning the count.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { collectModules, loadInto } from "spearkit";
|
|
94
|
+
|
|
95
|
+
// Inspect before registering:
|
|
96
|
+
const items = await collectModules(`${here}commands`);
|
|
97
|
+
console.log(`Found ${items.length} modules`);
|
|
98
|
+
client.register(...items);
|
|
99
|
+
|
|
100
|
+
// Or do both in one step:
|
|
101
|
+
const count = await loadInto(client, `${here}events`);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Example layout
|
|
105
|
+
|
|
106
|
+
The `examples/file-based-loading` project keeps each handler in its own file:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
file-based-loading/
|
|
110
|
+
index.ts # construct client, load each folder, start, deploy
|
|
111
|
+
commands/
|
|
112
|
+
ping.ts # export default command({ ... })
|
|
113
|
+
echo.ts # export default command({ ... })
|
|
114
|
+
events/
|
|
115
|
+
ready.ts # export default event("clientReady", ...)
|
|
116
|
+
components/
|
|
117
|
+
vote.ts # export const vote = button({ ... })
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A command file looks like this:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// commands/echo.ts
|
|
124
|
+
import { command, option } from "spearkit";
|
|
125
|
+
|
|
126
|
+
export default command({
|
|
127
|
+
name: "echo",
|
|
128
|
+
description: "Repeat a message",
|
|
129
|
+
options: {
|
|
130
|
+
text: option.string({ description: "What to say", required: true }),
|
|
131
|
+
loud: option.boolean({ description: "Shout it" }),
|
|
132
|
+
},
|
|
133
|
+
// text: string, loud: boolean | undefined
|
|
134
|
+
run: (ctx) => ctx.reply(ctx.options.loud ? ctx.options.text.toUpperCase() : ctx.options.text),
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Build the project, then run the compiled `index.js`; `client.load` will import the
|
|
139
|
+
emitted `.js` files and register `ping`, `echo`, `ready` and `vote` for you.
|
|
140
|
+
|
|
141
|
+
## See also
|
|
142
|
+
|
|
143
|
+
- [Client](./client.md) — `load`, `register`, and the registries the loader writes to.
|
|
144
|
+
- [Events](./events.md) — the `event()` helper that event modules export.
|
package/docs/logging.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Logging
|
|
2
|
+
|
|
3
|
+
spearkit ships a small, dependency-free structured logger. Every client owns one
|
|
4
|
+
at `client.logger`, and spearkit routes its own command, component, event, and
|
|
5
|
+
gateway errors through it. You can use the same logger for your code, or build a
|
|
6
|
+
standalone one.
|
|
7
|
+
|
|
8
|
+
## A first logger
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { Logger } from "spearkit";
|
|
12
|
+
|
|
13
|
+
const log = new Logger(); // level "info", logs to the console
|
|
14
|
+
log.info("bot starting");
|
|
15
|
+
log.error("connection lost", { error: new Error("ECONNRESET") });
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
A logger is constructed from `LoggerOptions`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { Logger } from "spearkit";
|
|
22
|
+
|
|
23
|
+
const log = new Logger({
|
|
24
|
+
level: "debug", // minimum severity to emit; default "info"
|
|
25
|
+
scope: "worker", // a prefix attached to every entry
|
|
26
|
+
// sink: consoleSink (the default)
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Levels
|
|
31
|
+
|
|
32
|
+
There are four levels, lowest to highest: `debug`, `info`, `warn`, `error`. The
|
|
33
|
+
logger emits an entry only if its level is at or above the configured threshold.
|
|
34
|
+
The default threshold is **`info`**, so `debug` entries are suppressed until you
|
|
35
|
+
lower it. A fifth threshold, `"silent"`, suppresses everything.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { Logger } from "spearkit";
|
|
39
|
+
|
|
40
|
+
const log = new Logger(); // threshold "info"
|
|
41
|
+
log.debug("only visible at debug"); // suppressed by default
|
|
42
|
+
log.info("visible");
|
|
43
|
+
|
|
44
|
+
log.setLevel("debug"); // now debug entries are emitted too
|
|
45
|
+
log.enabled("debug"); // true
|
|
46
|
+
log.setLevel("silent"); // suppress everything
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| Method | Level | Use for |
|
|
50
|
+
| ------ | ----- | ------- |
|
|
51
|
+
| `log.debug(msg, opts?)` | `debug` | Verbose diagnostics, off by default. |
|
|
52
|
+
| `log.info(msg, opts?)` | `info` | Normal operational messages. |
|
|
53
|
+
| `log.warn(msg, opts?)` | `warn` | Recoverable problems worth attention. |
|
|
54
|
+
| `log.error(msg, opts?)` | `error` | Failures; attach the cause via `{ error }`. |
|
|
55
|
+
|
|
56
|
+
`log.level` reads the current threshold, `log.setLevel(level)` changes it, and
|
|
57
|
+
`log.enabled(level)` reports whether an entry of that level would be emitted —
|
|
58
|
+
handy to guard expensive message construction.
|
|
59
|
+
|
|
60
|
+
## Scopes and child loggers
|
|
61
|
+
|
|
62
|
+
`log.child("scope")` returns a child logger whose entries carry an extra scope
|
|
63
|
+
segment. A child **shares its parent's threshold and sink**, so changing the
|
|
64
|
+
level on any logger in the tree affects them all.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { Logger } from "spearkit";
|
|
68
|
+
|
|
69
|
+
const log = new Logger({ scope: "app" });
|
|
70
|
+
const db = log.child("db"); // scope "app:db"
|
|
71
|
+
const cache = db.child("cache"); // scope "app:db:cache"
|
|
72
|
+
|
|
73
|
+
db.info("connected");
|
|
74
|
+
log.setLevel("debug"); // affects log, db, and cache
|
|
75
|
+
cache.debug("warm"); // now emitted
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
spearkit uses this internally: the client creates `commands`, `components`,
|
|
79
|
+
`events`, `scheduler`, `prefix`, and `usage` children off `client.logger`, so
|
|
80
|
+
every subsystem's output is scoped and a single `setLevel` controls them all.
|
|
81
|
+
|
|
82
|
+
## Structured `data` and `error`
|
|
83
|
+
|
|
84
|
+
Both arguments live in the optional second parameter (`LogOptions`):
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { Logger } from "spearkit";
|
|
88
|
+
|
|
89
|
+
const log = new Logger();
|
|
90
|
+
|
|
91
|
+
log.info("command finished", {
|
|
92
|
+
data: { command: "ping", ms: 12, cached: true },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
throw new Error("kaboom");
|
|
97
|
+
} catch (cause) {
|
|
98
|
+
log.error("handler failed", {
|
|
99
|
+
error: cause instanceof Error ? cause : new Error(String(cause)),
|
|
100
|
+
data: { command: "purge", guildId: "123" },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`data` is a flat record of primitives (`string | number | boolean | bigint |
|
|
106
|
+
null | undefined`). `error` is an `Error`; the default sink renders its stack.
|
|
107
|
+
|
|
108
|
+
### Coercing unknown throws
|
|
109
|
+
|
|
110
|
+
A `catch` binding is `unknown`. `toError(value)` turns any thrown value into an
|
|
111
|
+
`Error` so it fits `{ error }`:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { Logger, toError } from "spearkit";
|
|
115
|
+
|
|
116
|
+
const log = new Logger();
|
|
117
|
+
try {
|
|
118
|
+
JSON.parse("{");
|
|
119
|
+
} catch (cause) {
|
|
120
|
+
log.error("parse failed", { error: toError(cause) });
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Custom sinks
|
|
125
|
+
|
|
126
|
+
A sink is `(entry: LogEntry) => void`. The default is `consoleSink`, which writes
|
|
127
|
+
human-readable lines to the console (stderr for `warn`/`error`). Pass your own to
|
|
128
|
+
route entries anywhere — JSON lines, a file, an aggregator:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { Logger, type LogEntry } from "spearkit";
|
|
132
|
+
|
|
133
|
+
const log = new Logger({
|
|
134
|
+
level: "debug",
|
|
135
|
+
sink: (entry: LogEntry) => {
|
|
136
|
+
process.stdout.write(
|
|
137
|
+
JSON.stringify({
|
|
138
|
+
level: entry.level,
|
|
139
|
+
message: entry.message,
|
|
140
|
+
scope: entry.scope,
|
|
141
|
+
at: entry.timestamp.toISOString(),
|
|
142
|
+
data: entry.data,
|
|
143
|
+
error: entry.error?.message,
|
|
144
|
+
}) + "\n",
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
log.child("commands").info("dispatched", { data: { name: "ping" } });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
A `LogEntry` is the fully-resolved record handed to the sink:
|
|
153
|
+
|
|
154
|
+
| Field | Type | Notes |
|
|
155
|
+
| ----- | ---- | ----- |
|
|
156
|
+
| `level` | `LogLevel` | One of `debug`/`info`/`warn`/`error`. |
|
|
157
|
+
| `message` | `string` | The log message. |
|
|
158
|
+
| `scope` | `string \| undefined` | The accumulated scope, if any. |
|
|
159
|
+
| `timestamp` | `Date` | When the entry was created. |
|
|
160
|
+
| `error` | `Error \| undefined` | The attached error, if any. |
|
|
161
|
+
| `data` | `Record<string, LogValue> \| undefined` | The structured metadata, if any. |
|
|
162
|
+
|
|
163
|
+
## Configuring via the client
|
|
164
|
+
|
|
165
|
+
Pass `logger` to `SpearClient`. Give it `LoggerOptions` to build one, or a
|
|
166
|
+
`Logger` instance you already have:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { SpearClient, Logger } from "spearkit";
|
|
170
|
+
|
|
171
|
+
// Build from options:
|
|
172
|
+
const a = new SpearClient({ logger: { level: "debug" } });
|
|
173
|
+
|
|
174
|
+
// Or reuse an instance (e.g. one shared with non-Discord code):
|
|
175
|
+
const shared = new Logger({ level: "info", scope: "svc" });
|
|
176
|
+
const b = new SpearClient({ logger: shared });
|
|
177
|
+
|
|
178
|
+
a.logger.info("ready");
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The client logs all command, component, and event handler errors plus gateway
|
|
182
|
+
errors through `client.logger`. Set `level: "debug"` to see dispatch traces from
|
|
183
|
+
every subsystem:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { SpearClient } from "spearkit";
|
|
187
|
+
|
|
188
|
+
const client = new SpearClient({ logger: { level: "debug" } });
|
|
189
|
+
// client.logger.child("commands"), ".child('events')", etc. all log at debug now
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## See also
|
|
193
|
+
|
|
194
|
+
- [Client](./client.md) — the `logger` and other construction options.
|
|
195
|
+
- [Environment & dotenv](./env.md) — load configuration before you start.
|
package/docs/messages.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Messages & limits
|
|
2
|
+
|
|
3
|
+
Discord caps a message's `content` at **2000 characters**. Long output — a log
|
|
4
|
+
dump, a list, an AI response — silently fails or throws unless you split it.
|
|
5
|
+
spearkit ships two helpers (in addition to the duration/timestamp formatters; see
|
|
6
|
+
the [API reference](./api-reference.md)).
|
|
7
|
+
|
|
8
|
+
## Split long output
|
|
9
|
+
|
|
10
|
+
`chunkMessage(text, options?)` breaks text into chunks that each fit the limit,
|
|
11
|
+
preferring line boundaries (and word boundaries for an over-long single line) so
|
|
12
|
+
you never lose the tail:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { chunkMessage } from "spearkit";
|
|
16
|
+
|
|
17
|
+
const parts = chunkMessage(hugeLog); // default max = 2000
|
|
18
|
+
await ctx.reply(parts[0] ?? "(empty)");
|
|
19
|
+
for (const part of parts.slice(1)) await ctx.followUp(part);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Pass `{ max }` to target a smaller budget (e.g. inside a code block or embed
|
|
23
|
+
description). `MESSAGE_CHARACTER_LIMIT` (2000) is exported as the default.
|
|
24
|
+
|
|
25
|
+
## Truncate
|
|
26
|
+
|
|
27
|
+
`truncate(text, max, suffix?)` cuts text to `max` characters, appending the
|
|
28
|
+
suffix (default `…`) — the result, suffix included, never exceeds `max`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { truncate } from "spearkit";
|
|
32
|
+
|
|
33
|
+
embed.setFooter({ text: truncate(reason, 100) });
|
|
34
|
+
truncate("a very long reason", 10); // → "a very lo…"
|
|
35
|
+
```
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Migrating from discord.js
|
|
2
|
+
|
|
3
|
+
spearkit re-exports the entire discord.js surface, so adopting it is not a rewrite —
|
|
4
|
+
it is a one-line import change followed by *optional*, incremental cleanup. You can
|
|
5
|
+
move to spearkit today and start using its ergonomic helpers whenever you like.
|
|
6
|
+
|
|
7
|
+
## The drop-in story
|
|
8
|
+
|
|
9
|
+
Change `from "discord.js"` to `from "spearkit"`. Nothing else has to change: every
|
|
10
|
+
discord.js export is available under the same name with the same types.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// before
|
|
14
|
+
import { Client, EmbedBuilder, GatewayIntentBits } from "discord.js";
|
|
15
|
+
|
|
16
|
+
// after — identical behaviour
|
|
17
|
+
import { Client, EmbedBuilder, GatewayIntentBits } from "spearkit";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The full classic surface is there — builders, enums, the REST client, route
|
|
21
|
+
helpers, the `Events` map, and so on:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import {
|
|
25
|
+
ActionRowBuilder,
|
|
26
|
+
ButtonBuilder,
|
|
27
|
+
ButtonStyle,
|
|
28
|
+
Client,
|
|
29
|
+
EmbedBuilder,
|
|
30
|
+
Events,
|
|
31
|
+
GatewayIntentBits,
|
|
32
|
+
REST,
|
|
33
|
+
Routes,
|
|
34
|
+
SlashCommandBuilder,
|
|
35
|
+
} from "spearkit";
|
|
36
|
+
|
|
37
|
+
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
|
38
|
+
|
|
39
|
+
const pingCommand = new SlashCommandBuilder()
|
|
40
|
+
.setName("ping")
|
|
41
|
+
.setDescription("Replies with an embed and a button");
|
|
42
|
+
|
|
43
|
+
client.once(Events.ClientReady, (c) => {
|
|
44
|
+
console.log(`Ready as ${c.user.tag}`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
client.on(Events.InteractionCreate, async (interaction) => {
|
|
48
|
+
if (!interaction.isChatInputCommand()) return;
|
|
49
|
+
if (interaction.commandName !== "ping") return;
|
|
50
|
+
|
|
51
|
+
const embed = new EmbedBuilder().setTitle("Pong!").setDescription(`Latency: ${client.ws.ping}ms`);
|
|
52
|
+
const buttons = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
53
|
+
new ButtonBuilder().setCustomId("again").setLabel("Again").setStyle(ButtonStyle.Primary),
|
|
54
|
+
);
|
|
55
|
+
await interaction.reply({ embeds: [embed], components: [buttons] });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
async function deploy(token: string, appId: string): Promise<void> {
|
|
59
|
+
const rest = new REST().setToken(token);
|
|
60
|
+
await rest.put(Routes.applicationCommands(appId), { body: [pingCommand.toJSON()] });
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This file is 100% classic discord.js — only the import source changed. It keeps
|
|
65
|
+
working exactly as before.
|
|
66
|
+
|
|
67
|
+
## Incremental adoption
|
|
68
|
+
|
|
69
|
+
Once your imports point at spearkit, you can convert pieces one at a time. There is no
|
|
70
|
+
big-bang migration; old and new styles coexist.
|
|
71
|
+
|
|
72
|
+
1. **Swap the client.** Replace `new Client(...)` with `new SpearClient(...)`. It
|
|
73
|
+
*is* a discord.js `Client` (it extends it), so your existing `client.on`,
|
|
74
|
+
`client.once`, `client.ws`, `client.rest` code is unchanged — but now it also
|
|
75
|
+
routes interactions to spearkit's registries.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { SpearClient, Intents } from "spearkit";
|
|
79
|
+
|
|
80
|
+
const client = new SpearClient({ intents: Intents.default });
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
2. **Move commands to `command()`.** Replace a hand-written `SlashCommandBuilder`
|
|
84
|
+
plus its branch of the `interactionCreate` switch with a single co-located
|
|
85
|
+
definition. Option values become fully typed.
|
|
86
|
+
3. **Move events to `event()`.** Replace `client.on(Events.X, ...)` listeners with
|
|
87
|
+
`event("x", ...)` definitions and register them.
|
|
88
|
+
4. **Move components to spearkit builders.** Replace manual `ButtonBuilder` +
|
|
89
|
+
custom-id parsing with `button()`, `stringSelect()`, `modal()`, etc. — spearkit
|
|
90
|
+
routes them by custom-id namespace and decodes `{param}`s for you.
|
|
91
|
+
|
|
92
|
+
Convert at whatever pace suits you; un-migrated handlers keep running through your
|
|
93
|
+
existing `interactionCreate` listener.
|
|
94
|
+
|
|
95
|
+
## Before and after
|
|
96
|
+
|
|
97
|
+
The classic approach hand-routes every interaction through one big switch and
|
|
98
|
+
parses custom ids by hand:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// discord.js: one listener routes everything by hand
|
|
102
|
+
import { Client, Events, GatewayIntentBits } from "discord.js";
|
|
103
|
+
|
|
104
|
+
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
|
105
|
+
|
|
106
|
+
client.on(Events.InteractionCreate, async (interaction) => {
|
|
107
|
+
if (interaction.isChatInputCommand()) {
|
|
108
|
+
if (interaction.commandName === "greet") {
|
|
109
|
+
const who = interaction.options.getUser("who", true);
|
|
110
|
+
await interaction.reply(`Hello ${who}!`);
|
|
111
|
+
}
|
|
112
|
+
} else if (interaction.isButton()) {
|
|
113
|
+
const [name, choice] = interaction.customId.split(":"); // manual parsing
|
|
114
|
+
if (name === "vote") {
|
|
115
|
+
await interaction.update({ content: `You chose ${choice}` });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
spearkit co-locates each command and component with its handler, and routes
|
|
122
|
+
interactions for you — no switch, no manual id parsing:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// spearkit: each handler owns its definition; routing is automatic
|
|
126
|
+
import { SpearClient, Intents, command, option, button, row } from "spearkit";
|
|
127
|
+
|
|
128
|
+
const client = new SpearClient({ intents: Intents.default });
|
|
129
|
+
|
|
130
|
+
const greet = command({
|
|
131
|
+
name: "greet",
|
|
132
|
+
description: "Greet someone",
|
|
133
|
+
options: { who: option.user({ description: "Who to greet", required: true }) },
|
|
134
|
+
run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // who: User
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const vote = button({
|
|
138
|
+
id: "vote:{choice}", // {choice} is a typed param
|
|
139
|
+
label: "Yes",
|
|
140
|
+
style: "Success",
|
|
141
|
+
run: (ctx) => ctx.update(`You chose ${ctx.params.choice}`), // choice: string
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
client.register(greet, vote);
|
|
145
|
+
await client.start(process.env.DISCORD_TOKEN);
|
|
146
|
+
await client.deployCommands({ guildId: process.env.GUILD_ID });
|
|
147
|
+
|
|
148
|
+
// build() requires exactly the params the id pattern declares:
|
|
149
|
+
await channel.send({ content: "Vote:", components: [row(vote.build({ choice: "yes" }))] });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The option value (`who`) and the custom-id param (`choice`) are inferred from the
|
|
153
|
+
definitions — no casts, no `getUser`/`split` boilerplate, and no `interactionCreate`
|
|
154
|
+
switch to maintain.
|
|
155
|
+
|
|
156
|
+
## See also
|
|
157
|
+
|
|
158
|
+
- [Getting started](./getting-started.md) — install spearkit and build a first bot.
|
|
159
|
+
- [Commands](./commands.md) — define slash commands with typed options.
|
|
160
|
+
- [Components](./components.md) — buttons, selects, modals and custom-id routing.
|