pi-roundtable 0.7.19 → 0.9.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/CHANGELOG.md +140 -0
- package/README.md +72 -15
- package/README.zh-TW.md +65 -13
- package/docs/migrating-0.8.md +132 -0
- package/docs/migration-0.9.md +247 -0
- package/docs/plugins.md +853 -81
- package/examples/channel-context.test.ts +88 -0
- package/examples/channel-context.ts +62 -0
- package/examples/discord-adapter.test.ts +42 -0
- package/examples/discord-adapter.ts +26 -0
- package/examples/echo-runtime.test.ts +1 -0
- package/examples/echo-runtime.ts +1 -1
- package/examples/fake-surface.test.ts +69 -6
- package/examples/fake-surface.ts +73 -7
- package/examples/headless.test.ts +72 -0
- package/examples/headless.ts +35 -0
- package/examples/http.test.ts +31 -1
- package/examples/http.ts +39 -0
- package/examples/prechecks.test.ts +1 -0
- package/examples/principals.test.ts +60 -0
- package/examples/principals.ts +48 -0
- package/examples/prompt.test.ts +1 -1
- package/examples/study-room.test.ts +29 -1
- package/examples/study-room.ts +16 -19
- package/examples/support-desk.test.ts +3 -1
- package/examples/tools.test.ts +6 -1
- package/package.json +7 -3
- package/src/cli/checks/database.ts +11 -0
- package/src/cli/checks/identity.ts +225 -0
- package/src/cli/checks/model.ts +21 -3
- package/src/cli/cli.ts +130 -5
- package/src/cli/config-edit.ts +83 -13
- package/src/cli/doctor.ts +28 -5
- package/src/cli/init.ts +9 -1
- package/src/cli/line-diff.ts +72 -0
- package/src/cli/principal.ts +412 -0
- package/src/cli/project.ts +33 -2
- package/src/cli/report.ts +12 -3
- package/src/cli/templates.ts +46 -14
- package/src/cli/upgrade-rewrite.ts +102 -0
- package/src/cli/upgrade-source.ts +430 -0
- package/src/cli/upgrade.ts +176 -0
- package/src/core/agents/agent-claim.ts +56 -34
- package/src/core/agents/agent-team-fixture.ts +6 -0
- package/src/core/agents/team-turns.ts +10 -1
- package/src/core/builtin/agent-server.ts +38 -101
- package/src/core/builtin/discord-admin.ts +4 -1
- package/src/core/builtin/discord.ts +71 -13
- package/src/core/builtin/modules.ts +197 -43
- package/src/core/builtin/stores.ts +40 -21
- package/src/core/config/access.ts +229 -0
- package/src/core/config/adapters.ts +120 -0
- package/src/core/config/config.ts +228 -94
- package/src/core/contract/channel-context.ts +53 -0
- package/src/core/contract/channels.ts +81 -9
- package/src/core/contract/providers.ts +3 -2
- package/src/core/contract/runtime.ts +31 -11
- package/src/core/contract/surface.ts +35 -6
- package/src/core/conversations/conversation-registry.ts +56 -0
- package/src/core/conversations/conversation-store.ts +126 -0
- package/src/core/conversations/conversations-plugin.ts +29 -0
- package/src/core/define-roundtable.ts +157 -57
- package/src/core/discord/agent-panel.ts +2 -2
- package/src/core/discord/card-audience.ts +148 -0
- package/src/core/discord/channel-context.ts +348 -0
- package/src/core/discord/connection.ts +15 -3
- package/src/core/discord/direct-channel.ts +54 -0
- package/src/core/discord/discord-adapter.ts +9 -0
- package/src/core/discord/discord-owners.ts +133 -0
- package/src/core/discord/discord-surface.ts +69 -14
- package/src/core/discord/fresh-marker.ts +24 -0
- package/src/core/discord/inbound-message.ts +21 -7
- package/src/core/discord/interaction-module.ts +11 -1
- package/src/core/discord/owner-cards-fixture.ts +80 -0
- package/src/core/discord/owner-cards.ts +58 -86
- package/src/core/discord/owner-command.ts +27 -13
- package/src/core/discord/stop-button.ts +4 -4
- package/src/core/domain/conversation.ts +10 -0
- package/src/core/domain/errors.ts +5 -0
- package/src/core/domain/ports.ts +33 -2
- package/src/core/domain/progress.ts +10 -0
- package/src/core/events.ts +1 -0
- package/src/core/host.ts +32 -10
- package/src/core/http/listeners.ts +174 -21
- package/src/core/http/websocket.ts +316 -0
- package/src/core/i18n/cards.ts +11 -9
- package/src/core/identity/access-policy.ts +227 -0
- package/src/core/identity/actor-facts.ts +65 -0
- package/src/core/identity/caching-principal-store.ts +111 -0
- package/src/core/identity/config-owner.ts +145 -0
- package/src/core/identity/contact.ts +27 -0
- package/src/core/identity/identity-plugin.ts +56 -0
- package/src/core/identity/identity-schema.ts +282 -0
- package/src/core/identity/identity-service.ts +489 -0
- package/src/core/identity/identity-view.ts +77 -0
- package/src/core/identity/plugin-identities.ts +109 -0
- package/src/core/identity/principal-reads.ts +38 -0
- package/src/core/identity/principal-store.ts +478 -0
- package/src/core/identity/principal-tables.ts +53 -0
- package/src/core/identity/seen-throttle.ts +57 -0
- package/src/core/identity/ulid.ts +36 -0
- package/src/core/identity.ts +34 -5
- package/src/core/interactions/prompts.ts +127 -0
- package/src/core/modules/background/background-turns.ts +56 -17
- package/src/core/modules/background/personal-target.ts +50 -0
- package/src/core/modules/delegation/delegate.ts +28 -14
- package/src/core/modules/delegation/delegator.ts +23 -7
- package/src/core/modules/memory/owner-memory.ts +123 -50
- package/src/core/modules/notify/notify.ts +53 -13
- package/src/core/modules/notify/session-notify.ts +110 -0
- package/src/core/modules/schedules/prechecks.ts +8 -2
- package/src/core/modules/schedules/schedule-prechecks.ts +4 -1
- package/src/core/modules/schedules/schedule-store.ts +51 -20
- package/src/core/modules/schedules/schedule-tools.ts +122 -12
- package/src/core/modules/schedules/scheduler.ts +43 -2
- package/src/core/modules/schedules/schedules.ts +43 -14
- package/src/core/ops/error-reporter.ts +24 -12
- package/src/core/plugin.ts +62 -0
- package/src/core/presence/direct-channels.ts +104 -0
- package/src/core/registry/contributions.ts +63 -3
- package/src/core/registry/providers.ts +1 -1
- package/src/core/routing/channel-router.ts +226 -17
- package/src/core/routing/conversation-turns.ts +85 -1
- package/src/core/routing/surface-port.ts +35 -2
- package/src/core/routing/system-turns.ts +29 -0
- package/src/core/runtime/bridge-guard.ts +225 -0
- package/src/core/runtime/compaction-tiers.ts +5 -1
- package/src/core/runtime/conversation-sessions.ts +124 -12
- package/src/core/runtime/extensions/ask-user.ts +1 -1
- package/src/core/runtime/extensions/confirmation-gate.ts +32 -10
- package/src/core/runtime/extensions/private-memory.ts +447 -0
- package/src/core/runtime/extensions/self-compact-guard.ts +11 -0
- package/src/core/runtime/pending-confirmation-store.ts +67 -4
- package/src/core/runtime/pi-agent-runtime.ts +143 -135
- package/src/core/runtime/preflight-tools.ts +47 -0
- package/src/core/runtime/prompt-slot.ts +6 -6
- package/src/core/runtime/reader-history.ts +129 -0
- package/src/core/runtime/runtime-plugin.ts +205 -0
- package/src/core/runtime/runtime-types.ts +40 -12
- package/src/core/runtime/session-conversation.ts +143 -0
- package/src/core/runtime/session-factory.ts +115 -15
- package/src/core/runtime/session-scope.ts +78 -0
- package/src/core/runtime/steerable-run.ts +9 -0
- package/src/core/runtime/tool-call-scope.ts +43 -0
- package/src/core/runtime/turn-answer.ts +30 -1
- package/src/core/runtime/turn-progress.ts +116 -0
- package/src/core/services.ts +42 -2
- package/src/core/sessions.ts +30 -0
- package/src/core/shared/unix-server.ts +35 -5
- package/src/core/speakers.ts +21 -19
- package/src/core/testing/background.ts +31 -0
- package/src/core/testing/fixture-database.ts +55 -0
- package/src/core/testing/map-identity.ts +76 -0
- package/src/core/testing/module-sessions.ts +134 -0
- package/src/core/testing/modules.ts +149 -38
- package/src/core/testing/owner.ts +17 -0
- package/src/core/testing/surface-contract.ts +460 -0
- package/src/core/testing/test-host.ts +100 -19
- package/src/core/testing/tool-set.ts +32 -0
- package/src/core/tool-set.snapshot.json +4 -4
- package/src/core/tool-tiers.ts +57 -3
- package/src/discord/index.ts +10 -0
- package/src/index.ts +65 -8
- package/src/kit/compaction.ts +13 -0
- package/src/kit/index.ts +10 -0
- package/src/kit/support.ts +1 -1
- package/src/testing.ts +96 -21
- package/templates/.env.example +1 -1
- package/templates/adapters/web/.env.example +24 -0
- package/templates/adapters/web/README.md +22 -0
- package/templates/adapters/web/package.json.tmpl +26 -0
- package/templates/adapters/web/persona/assistant.md +2 -0
- package/templates/adapters/web/roundtable.config.ts +59 -0
- package/templates/package.json.tmpl +1 -0
- package/templates/plugins/self-compact.ts +11 -0
- package/templates/roundtable.config.ts +14 -2
- package/src/core/domain/owner-prompts.ts +0 -45
package/src/cli/cli.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { resolveConfig } from "../core/config/config.ts";
|
|
3
4
|
import type { DefinedRoundtable } from "../core/define-roundtable.ts";
|
|
4
5
|
import { Roundtable } from "../core/host.ts";
|
|
6
|
+
import { declaredIdentities } from "../core/identity/plugin-identities.ts";
|
|
5
7
|
import { addPackage } from "./add-package.ts";
|
|
6
8
|
import { addPlugin } from "./add-plugin.ts";
|
|
7
9
|
import type { BunFacts } from "./checks/bun.ts";
|
|
@@ -10,11 +12,24 @@ import { doctor } from "./doctor.ts";
|
|
|
10
12
|
import { fetchHttp } from "./http.ts";
|
|
11
13
|
import { init } from "./init.ts";
|
|
12
14
|
import { type PackagePorts, piPackagePorts } from "./pi-packages.ts";
|
|
13
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
type Configured,
|
|
17
|
+
type ConfiguredIdentities,
|
|
18
|
+
PRINCIPAL_USAGE,
|
|
19
|
+
principalCommand,
|
|
20
|
+
} from "./principal.ts";
|
|
21
|
+
import {
|
|
22
|
+
CONFIG_FILE,
|
|
23
|
+
loadConfigFile,
|
|
24
|
+
loadDefault,
|
|
25
|
+
type Ports,
|
|
26
|
+
Project,
|
|
27
|
+
} from "./project.ts";
|
|
14
28
|
import { formatOutcomes } from "./report.ts";
|
|
15
29
|
import { assemble, providerLogin } from "./runtime.ts";
|
|
16
30
|
import { start } from "./start.ts";
|
|
17
|
-
import { OFFICIAL_PLUGINS } from "./templates.ts";
|
|
31
|
+
import { ADAPTERS, isAdapter, OFFICIAL_PLUGINS } from "./templates.ts";
|
|
32
|
+
import { upgrade } from "./upgrade.ts";
|
|
18
33
|
|
|
19
34
|
/** What the command line reads from its surroundings; tests replace every part. */
|
|
20
35
|
export interface CliEnvironment {
|
|
@@ -32,7 +47,9 @@ export interface CliEnvironment {
|
|
|
32
47
|
|
|
33
48
|
const USAGE = `roundtable: a Discord agent server on Pi
|
|
34
49
|
|
|
35
|
-
roundtable init [dir]
|
|
50
|
+
roundtable init [dir] [--adapter discord|web]
|
|
51
|
+
create a project (default: the current directory) that talks
|
|
52
|
+
through Discord (the default) or through the web chat
|
|
36
53
|
roundtable doctor [--reachable] check the setup and say how to fix what is wrong
|
|
37
54
|
roundtable start run the checks that need no network, then the bot
|
|
38
55
|
roundtable add plugin <name> add plugins/<name>.ts and its test, and list it in the config
|
|
@@ -40,6 +57,12 @@ const USAGE = `roundtable: a Discord agent server on Pi
|
|
|
40
57
|
which are copied in ready to run instead of the template
|
|
41
58
|
roundtable add package <spec> install a Pi package with bun add, and add plugins/<name>.ts that
|
|
42
59
|
loads it and gives its tools to every agent turn
|
|
60
|
+
roundtable upgrade [--write [--unchecked]]
|
|
61
|
+
show roundtable.config.ts rewritten in the 0.9 form (owner and
|
|
62
|
+
speakers as access, discord as an adapter); --write writes it once
|
|
63
|
+
it is checked to serve the same people, which needs the
|
|
64
|
+
environment the host runs with; --unchecked writes it unchecked
|
|
65
|
+
${PRINCIPAL_USAGE}
|
|
43
66
|
`;
|
|
44
67
|
|
|
45
68
|
/** The package's own version and the Bun range it needs, from the package.json beside the source. */
|
|
@@ -108,13 +131,22 @@ export async function runCli(
|
|
|
108
131
|
return 0;
|
|
109
132
|
}
|
|
110
133
|
if (command === "init") {
|
|
111
|
-
const
|
|
112
|
-
|
|
134
|
+
const at = rest.indexOf("--adapter");
|
|
135
|
+
const adapter = at === -1 ? "discord" : rest[at + 1];
|
|
136
|
+
if (adapter === undefined || !isAdapter(adapter))
|
|
137
|
+
return usage(
|
|
138
|
+
io,
|
|
139
|
+
`--adapter takes one of ${ADAPTERS.join(", ")}, not ${JSON.stringify(adapter ?? "")}`,
|
|
140
|
+
);
|
|
141
|
+
const [dir, ...extra] = at === -1 ? rest : rest.toSpliced(at, 2);
|
|
142
|
+
if (extra.length > 0 || dir?.startsWith("-"))
|
|
143
|
+
return usage(io, "init takes at most one directory");
|
|
113
144
|
const report = init({
|
|
114
145
|
cwd: io.cwd,
|
|
115
146
|
...(dir === undefined ? {} : { dir }),
|
|
116
147
|
bun: io.bun(),
|
|
117
148
|
version: io.version(),
|
|
149
|
+
adapter,
|
|
118
150
|
});
|
|
119
151
|
if (!report.ok) {
|
|
120
152
|
for (const line of report.problems) io.err(line);
|
|
@@ -190,9 +222,102 @@ export async function runCli(
|
|
|
190
222
|
}
|
|
191
223
|
return 0;
|
|
192
224
|
}
|
|
225
|
+
if (command === "upgrade") {
|
|
226
|
+
const write = rest.includes("--write");
|
|
227
|
+
const unchecked = rest.includes("--unchecked");
|
|
228
|
+
if (rest.some((arg) => arg !== "--write" && arg !== "--unchecked"))
|
|
229
|
+
return usage(io, "upgrade takes only --write and --unchecked");
|
|
230
|
+
if (unchecked && !write) return usage(io, "--unchecked goes with --write");
|
|
231
|
+
return runUpgrade(io, write, unchecked);
|
|
232
|
+
}
|
|
233
|
+
if (command === "principal") {
|
|
234
|
+
const project = new Project(resolve(io.cwd), io.ports);
|
|
235
|
+
const raw = await project.raw();
|
|
236
|
+
if (!raw.ok) {
|
|
237
|
+
for (const line of formatOutcomes([
|
|
238
|
+
{ name: "configuration", result: raw.failure },
|
|
239
|
+
]))
|
|
240
|
+
io.err(line);
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
const url = await project.text("database", "url");
|
|
244
|
+
if (!url) {
|
|
245
|
+
io.err(
|
|
246
|
+
`database.url has no value in ${CONFIG_FILE}; the principals live in the host's database. Set DATABASE_URL in .env.`,
|
|
247
|
+
);
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
return principalCommand(url, rest, io, configured(raw.value));
|
|
251
|
+
}
|
|
193
252
|
return usage(io, `unknown command ${JSON.stringify(command)}`);
|
|
194
253
|
}
|
|
195
254
|
|
|
255
|
+
/**
|
|
256
|
+
* The owners the configuration lists and the identities its plugins declare, read by its schema
|
|
257
|
+
* alone: its plugins and model runtime are not assembled, so their failures do not pass for a
|
|
258
|
+
* configuration that does not load. Identities the start would refuse are unknown.
|
|
259
|
+
*/
|
|
260
|
+
function configured(value: unknown): Configured {
|
|
261
|
+
let config: ReturnType<typeof resolveConfig>;
|
|
262
|
+
try {
|
|
263
|
+
config = resolveConfig(value);
|
|
264
|
+
} catch {
|
|
265
|
+
return { owners: undefined, identities: undefined };
|
|
266
|
+
}
|
|
267
|
+
let identities: ConfiguredIdentities;
|
|
268
|
+
try {
|
|
269
|
+
identities = declaredIdentities(config.plugins);
|
|
270
|
+
} catch {
|
|
271
|
+
identities = undefined;
|
|
272
|
+
}
|
|
273
|
+
return { owners: config.access.owners, identities };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function runUpgrade(
|
|
277
|
+
io: CliEnvironment,
|
|
278
|
+
write: boolean,
|
|
279
|
+
unchecked: boolean,
|
|
280
|
+
): Promise<number> {
|
|
281
|
+
const report = await upgrade({
|
|
282
|
+
cwd: resolve(io.cwd),
|
|
283
|
+
write,
|
|
284
|
+
unchecked,
|
|
285
|
+
load: loadDefault,
|
|
286
|
+
});
|
|
287
|
+
if (!report.ok) {
|
|
288
|
+
for (const line of report.problems) io.err(line);
|
|
289
|
+
io.err(`${CONFIG_FILE} was not changed.`);
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
292
|
+
if (report.changes.length === 0) {
|
|
293
|
+
io.out(
|
|
294
|
+
`${CONFIG_FILE} is already in the 0.9 form; there is nothing to change.`,
|
|
295
|
+
);
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
io.out(`${CONFIG_FILE} in the 0.9 form:`);
|
|
299
|
+
for (const change of report.changes) io.out(` - ${change}`);
|
|
300
|
+
io.out("");
|
|
301
|
+
for (const line of report.diff) io.out(line);
|
|
302
|
+
io.out("");
|
|
303
|
+
for (const note of report.notes) io.out(`Note: ${note}.`);
|
|
304
|
+
io.out(
|
|
305
|
+
"same" in report.verified
|
|
306
|
+
? "Checked: the rewrite serves the same owners, admins, and members, with the same Discord settings."
|
|
307
|
+
: `Not checked: ${report.verified.skipped}.`,
|
|
308
|
+
);
|
|
309
|
+
if (report.written && "skipped" in report.verified)
|
|
310
|
+
io.err(
|
|
311
|
+
`Warning: written without the check, as --unchecked asked. Nothing confirmed that the host serves the same owners, admins, and members as before; read the diff above, and run roundtable doctor with the environment the host runs with.`,
|
|
312
|
+
);
|
|
313
|
+
io.out(
|
|
314
|
+
report.written
|
|
315
|
+
? `Wrote ${CONFIG_FILE}; no other file was changed.`
|
|
316
|
+
: `Nothing was written. Run roundtable upgrade --write to write it.`,
|
|
317
|
+
);
|
|
318
|
+
return 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
196
321
|
function usage(io: CliEnvironment, message: string): number {
|
|
197
322
|
io.err(message);
|
|
198
323
|
io.err(USAGE);
|
package/src/cli/config-edit.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parse } from "@babel/parser";
|
|
2
2
|
|
|
3
3
|
/** The parts of a syntax node this edit reads; the parser's own types are not needed beyond these. */
|
|
4
|
-
interface Node {
|
|
4
|
+
export interface Node {
|
|
5
5
|
type: string;
|
|
6
6
|
start: number;
|
|
7
7
|
end: number;
|
|
@@ -20,7 +20,7 @@ const WRAPPERS = new Set([
|
|
|
20
20
|
"ParenthesizedExpression",
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
|
-
const child = (node: Node, key: string): Node | undefined => {
|
|
23
|
+
export const child = (node: Node, key: string): Node | undefined => {
|
|
24
24
|
const value = node[key];
|
|
25
25
|
return typeof value === "object" && value !== null
|
|
26
26
|
? (value as Node)
|
|
@@ -28,35 +28,53 @@ const child = (node: Node, key: string): Node | undefined => {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
/** The expression under `satisfies`, `as`, `!`, and parentheses. */
|
|
31
|
-
function unwrap(node: Node | undefined): Node | undefined {
|
|
31
|
+
export function unwrap(node: Node | undefined): Node | undefined {
|
|
32
32
|
let current = node;
|
|
33
33
|
while (current && WRAPPERS.has(current.type))
|
|
34
34
|
current = child(current, "expression");
|
|
35
35
|
return current;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/** A parsed module: its top-level statements, and every comment in it. */
|
|
39
|
+
export interface ParsedModule {
|
|
40
|
+
body: Node[];
|
|
41
|
+
comments: Node[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The module parsed, or a ConfigEditError saying it does not parse and what to do by hand, `instead`. */
|
|
45
|
+
export function parseConfig(
|
|
46
|
+
source: string,
|
|
47
|
+
file: string,
|
|
48
|
+
instead: string,
|
|
49
|
+
): ParsedModule {
|
|
39
50
|
try {
|
|
40
|
-
|
|
41
|
-
return parse(source, {
|
|
51
|
+
const parsed = parse(source, {
|
|
42
52
|
sourceType: "module",
|
|
43
53
|
plugins: ["typescript"],
|
|
44
|
-
})
|
|
54
|
+
});
|
|
55
|
+
// SAFETY: every node the parser builds has a type, start, and end, which is all Node states.
|
|
56
|
+
return {
|
|
57
|
+
body: parsed.program.body as unknown as Node[],
|
|
58
|
+
comments: (parsed.comments ?? []) as unknown as Node[],
|
|
59
|
+
};
|
|
45
60
|
} catch (error) {
|
|
46
61
|
throw new ConfigEditError(
|
|
47
|
-
`${file} does not parse (${error instanceof Error ? error.message : String(error)}). Fix it, or
|
|
62
|
+
`${file} does not parse (${error instanceof Error ? error.message : String(error)}). Fix it, or ${instead}.`,
|
|
48
63
|
);
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
const
|
|
67
|
+
const parseModule = (source: string, file: string): Node[] =>
|
|
68
|
+
parseConfig(source, file, "add the plugin to its list by hand").body;
|
|
69
|
+
|
|
70
|
+
export const nameOf = (node: Node | undefined): string | undefined => {
|
|
53
71
|
if (node?.type === "Identifier") return node.name as string;
|
|
54
72
|
if (node?.type === "StringLiteral") return node.value as string;
|
|
55
73
|
return undefined;
|
|
56
74
|
};
|
|
57
75
|
|
|
58
76
|
/** Every name the module already binds at its top level, so an import cannot shadow one. */
|
|
59
|
-
function boundNames(body: readonly Node[]): Set<string> {
|
|
77
|
+
export function boundNames(body: readonly Node[]): Set<string> {
|
|
60
78
|
const names = new Set<string>();
|
|
61
79
|
for (const statement of body) {
|
|
62
80
|
const declaration =
|
|
@@ -80,7 +98,7 @@ function boundNames(body: readonly Node[]): Set<string> {
|
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
/** The object the config exports: the default export itself, or the top-level constant it names. */
|
|
83
|
-
function exportedObject(body: readonly Node[]): Node | undefined {
|
|
101
|
+
export function exportedObject(body: readonly Node[]): Node | undefined {
|
|
84
102
|
const exported = body.find(
|
|
85
103
|
(node) => node.type === "ExportDefaultDeclaration",
|
|
86
104
|
);
|
|
@@ -112,7 +130,7 @@ function pluginList(object: Node): Node | undefined {
|
|
|
112
130
|
return undefined;
|
|
113
131
|
}
|
|
114
132
|
|
|
115
|
-
interface Insertion {
|
|
133
|
+
export interface Insertion {
|
|
116
134
|
at: number;
|
|
117
135
|
text: string;
|
|
118
136
|
/** How many characters from `at` the text replaces; none when it only inserts. */
|
|
@@ -162,7 +180,11 @@ function expandedList(
|
|
|
162
180
|
}
|
|
163
181
|
|
|
164
182
|
/** The text to add so `ident` joins the list, in the list's own layout. */
|
|
165
|
-
function listInsertion(
|
|
183
|
+
export function listInsertion(
|
|
184
|
+
source: string,
|
|
185
|
+
list: Node,
|
|
186
|
+
ident: string,
|
|
187
|
+
): Insertion {
|
|
166
188
|
const elements = (list.elements as (Node | null)[]).filter(
|
|
167
189
|
(element): element is Node => element !== null,
|
|
168
190
|
);
|
|
@@ -205,6 +227,54 @@ function importInsertion(
|
|
|
205
227
|
: { at: 0, text: `${line}\n\n` };
|
|
206
228
|
}
|
|
207
229
|
|
|
230
|
+
/** Where an import of a package goes: after the package imports that sort before it, else first. */
|
|
231
|
+
export function packageImport(
|
|
232
|
+
body: readonly Node[],
|
|
233
|
+
specifier: string,
|
|
234
|
+
line: string,
|
|
235
|
+
): Insertion {
|
|
236
|
+
const imports = body.filter((node) => node.type === "ImportDeclaration");
|
|
237
|
+
const from = (node: Node) => String(child(node, "source")?.value ?? "");
|
|
238
|
+
const before = imports
|
|
239
|
+
.filter((node) => !from(node).startsWith(".") && from(node) < specifier)
|
|
240
|
+
.at(-1);
|
|
241
|
+
if (before) return { at: before.end, text: `\n${line}` };
|
|
242
|
+
const first = imports[0];
|
|
243
|
+
return first
|
|
244
|
+
? { at: first.start, text: `${line}\n` }
|
|
245
|
+
: { at: 0, text: `${line}\n\n` };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Whether the module imports `name` from `specifier` under that same name. */
|
|
249
|
+
export function importsName(
|
|
250
|
+
body: readonly Node[],
|
|
251
|
+
specifier: string,
|
|
252
|
+
name: string,
|
|
253
|
+
): boolean {
|
|
254
|
+
return body.some(
|
|
255
|
+
(node) =>
|
|
256
|
+
node.type === "ImportDeclaration" &&
|
|
257
|
+
child(node, "source")?.value === specifier &&
|
|
258
|
+
(node.specifiers as Node[]).some(
|
|
259
|
+
(imported) =>
|
|
260
|
+
imported.type === "ImportSpecifier" &&
|
|
261
|
+
nameOf(child(imported, "imported")) === name &&
|
|
262
|
+
nameOf(child(imported, "local")) === name,
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Whether the file imports anything from `specifier`. */
|
|
268
|
+
export const importsFrom = (
|
|
269
|
+
body: readonly Node[],
|
|
270
|
+
specifier: string,
|
|
271
|
+
): boolean =>
|
|
272
|
+
body.some(
|
|
273
|
+
(node) =>
|
|
274
|
+
node.type === "ImportDeclaration" &&
|
|
275
|
+
child(node, "source")?.value === specifier,
|
|
276
|
+
);
|
|
277
|
+
|
|
208
278
|
/**
|
|
209
279
|
* Adds the plugin's import line and lists it in the `plugins` array of the default export, in the
|
|
210
280
|
* file's own layout; a one-line list that would pass 80 columns is put one element a line. It
|
package/src/cli/doctor.ts
CHANGED
|
@@ -9,12 +9,20 @@ import {
|
|
|
9
9
|
Discord,
|
|
10
10
|
} from "./checks/discord.ts";
|
|
11
11
|
import { checkEnvironment } from "./checks/environment.ts";
|
|
12
|
+
import { checkAccess, checkPrincipals } from "./checks/identity.ts";
|
|
12
13
|
import { checkImageProvider } from "./checks/images.ts";
|
|
13
14
|
import { checkModelLogin } from "./checks/model.ts";
|
|
14
15
|
import { checkPublicUrl } from "./checks/public-url.ts";
|
|
15
16
|
import type { Http } from "./http.ts";
|
|
16
17
|
import { type Ports, Project } from "./project.ts";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
type Check,
|
|
20
|
+
failed,
|
|
21
|
+
type Outcome,
|
|
22
|
+
type Result,
|
|
23
|
+
runChecks,
|
|
24
|
+
skipped,
|
|
25
|
+
} from "./report.ts";
|
|
18
26
|
|
|
19
27
|
export interface DoctorInputs {
|
|
20
28
|
cwd: string;
|
|
@@ -39,6 +47,11 @@ export function buildChecks(
|
|
|
39
47
|
): Check[] {
|
|
40
48
|
const { cwd, env, bun, ports, database, http, reachable } = inputs;
|
|
41
49
|
const discord = new Discord(project, http);
|
|
50
|
+
// A project without Discord asks Discord nothing.
|
|
51
|
+
const ifDiscord = (run: () => Promise<Result>) => async (): Promise<Result> =>
|
|
52
|
+
(await project.leavesOut("discord"))
|
|
53
|
+
? skipped("no Discord is configured")
|
|
54
|
+
: run();
|
|
42
55
|
return [
|
|
43
56
|
{ name: "Bun", offline: true, run: async () => checkBun(bun) },
|
|
44
57
|
{
|
|
@@ -52,6 +65,7 @@ export function buildChecks(
|
|
|
52
65
|
run: () => checkConfiguration(project),
|
|
53
66
|
},
|
|
54
67
|
{ name: "plugins", offline: true, run: () => checkPlugins(project) },
|
|
68
|
+
{ name: "access", offline: true, run: () => checkAccess(project) },
|
|
55
69
|
{
|
|
56
70
|
name: "image provider",
|
|
57
71
|
offline: true,
|
|
@@ -62,21 +76,30 @@ export function buildChecks(
|
|
|
62
76
|
offline: false,
|
|
63
77
|
run: () => checkDatabase(project, database),
|
|
64
78
|
},
|
|
65
|
-
{
|
|
79
|
+
{
|
|
80
|
+
name: "principals",
|
|
81
|
+
offline: false,
|
|
82
|
+
run: () => checkPrincipals(project, database),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "Discord token",
|
|
86
|
+
offline: false,
|
|
87
|
+
run: ifDiscord(() => checkToken(discord)),
|
|
88
|
+
},
|
|
66
89
|
{
|
|
67
90
|
name: "Discord guild",
|
|
68
91
|
offline: false,
|
|
69
|
-
run: () => checkGuild(project, discord),
|
|
92
|
+
run: ifDiscord(() => checkGuild(project, discord)),
|
|
70
93
|
},
|
|
71
94
|
{
|
|
72
95
|
name: "Discord intents",
|
|
73
96
|
offline: false,
|
|
74
|
-
run: () => checkIntents(discord),
|
|
97
|
+
run: ifDiscord(() => checkIntents(discord)),
|
|
75
98
|
},
|
|
76
99
|
{
|
|
77
100
|
name: "Discord channel",
|
|
78
101
|
offline: false,
|
|
79
|
-
run: () => checkChannel(project, discord),
|
|
102
|
+
run: ifDiscord(() => checkChannel(project, discord)),
|
|
80
103
|
},
|
|
81
104
|
{
|
|
82
105
|
name: "model login",
|
package/src/cli/init.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { basename, join, relative, resolve } from "node:path";
|
|
3
3
|
import { type BunFacts, checkBun } from "./checks/bun.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type Adapter,
|
|
6
|
+
type Rendered,
|
|
7
|
+
renderProject,
|
|
8
|
+
writeRendered,
|
|
9
|
+
} from "./templates.ts";
|
|
5
10
|
|
|
6
11
|
export interface InitInputs {
|
|
7
12
|
cwd: string;
|
|
@@ -12,6 +17,8 @@ export interface InitInputs {
|
|
|
12
17
|
version: string;
|
|
13
18
|
/** Where the templates are; default the package's own. */
|
|
14
19
|
templates?: string;
|
|
20
|
+
/** The chat network the project talks through; default Discord. */
|
|
21
|
+
adapter?: Adapter;
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
export interface InitReport {
|
|
@@ -61,6 +68,7 @@ export function init(inputs: InitInputs): InitReport {
|
|
|
61
68
|
const files: Rendered[] = renderProject(
|
|
62
69
|
{ project: projectName(root), version: inputs.version },
|
|
63
70
|
inputs.templates,
|
|
71
|
+
inputs.adapter,
|
|
64
72
|
);
|
|
65
73
|
const existing = files
|
|
66
74
|
.map((file) => file.path)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Every line of `before` and `after` in order: kept, removed, or added. */
|
|
2
|
+
function lineOps(
|
|
3
|
+
before: readonly string[],
|
|
4
|
+
after: readonly string[],
|
|
5
|
+
): { kind: " " | "-" | "+"; line: string }[] {
|
|
6
|
+
// The longest common subsequence, from the end, so the walk below reads forward.
|
|
7
|
+
const rows = before.length + 1;
|
|
8
|
+
const columns = after.length + 1;
|
|
9
|
+
const common = new Uint32Array(rows * columns);
|
|
10
|
+
for (let i = before.length - 1; i >= 0; i--)
|
|
11
|
+
for (let j = after.length - 1; j >= 0; j--)
|
|
12
|
+
common[i * columns + j] =
|
|
13
|
+
before[i] === after[j]
|
|
14
|
+
? (common[(i + 1) * columns + j + 1] ?? 0) + 1
|
|
15
|
+
: Math.max(
|
|
16
|
+
common[(i + 1) * columns + j] ?? 0,
|
|
17
|
+
common[i * columns + j + 1] ?? 0,
|
|
18
|
+
);
|
|
19
|
+
const ops: { kind: " " | "-" | "+"; line: string }[] = [];
|
|
20
|
+
let i = 0;
|
|
21
|
+
let j = 0;
|
|
22
|
+
while (i < before.length || j < after.length) {
|
|
23
|
+
if (i < before.length && j < after.length && before[i] === after[j]) {
|
|
24
|
+
ops.push({ kind: " ", line: before[i++] ?? "" });
|
|
25
|
+
j++;
|
|
26
|
+
} else if (
|
|
27
|
+
j < after.length &&
|
|
28
|
+
(i === before.length ||
|
|
29
|
+
(common[i * columns + j + 1] ?? 0) >
|
|
30
|
+
(common[(i + 1) * columns + j] ?? 0))
|
|
31
|
+
)
|
|
32
|
+
ops.push({ kind: "+", line: after[j++] ?? "" });
|
|
33
|
+
else ops.push({ kind: "-", line: before[i++] ?? "" });
|
|
34
|
+
}
|
|
35
|
+
return ops;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const CONTEXT = 3;
|
|
39
|
+
|
|
40
|
+
/** The change from `before` to `after` as a unified diff with three lines of context; empty when they are the same. */
|
|
41
|
+
export function lineDiff(
|
|
42
|
+
before: string,
|
|
43
|
+
after: string,
|
|
44
|
+
file: string,
|
|
45
|
+
): string[] {
|
|
46
|
+
const ops = lineOps(before.split("\n"), after.split("\n"));
|
|
47
|
+
const changed = ops.flatMap((op, index) => (op.kind === " " ? [] : [index]));
|
|
48
|
+
if (changed.length === 0) return [];
|
|
49
|
+
// Changes closer than twice the context share a hunk.
|
|
50
|
+
const hunks: [number, number][] = [];
|
|
51
|
+
for (const index of changed) {
|
|
52
|
+
const last = hunks.at(-1);
|
|
53
|
+
if (last && index - last[1] <= 2 * CONTEXT) last[1] = index;
|
|
54
|
+
else hunks.push([index, index]);
|
|
55
|
+
}
|
|
56
|
+
const lines = [`--- ${file}`, `+++ ${file} (upgraded)`];
|
|
57
|
+
for (const [first, last] of hunks) {
|
|
58
|
+
const start = Math.max(0, first - CONTEXT);
|
|
59
|
+
const end = Math.min(ops.length - 1, last + CONTEXT);
|
|
60
|
+
const before = ops.slice(0, start);
|
|
61
|
+
const span = ops.slice(start, end + 1);
|
|
62
|
+
const count = (list: typeof ops, kind: "-" | "+") =>
|
|
63
|
+
list.filter((op) => op.kind !== (kind === "-" ? "+" : "-")).length;
|
|
64
|
+
const oldLength = count(span, "-");
|
|
65
|
+
const newLength = count(span, "+");
|
|
66
|
+
lines.push(
|
|
67
|
+
`@@ -${count(before, "-") + (oldLength > 0 ? 1 : 0)},${oldLength} +${count(before, "+") + (newLength > 0 ? 1 : 0)},${newLength} @@`,
|
|
68
|
+
...span.map((op) => `${op.kind}${op.line}`),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return lines;
|
|
72
|
+
}
|