@oh-my-pi/pi-coding-agent 15.11.7 → 15.12.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.
Files changed (107) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/dist/cli.js +8106 -7708
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/collab/crypto.d.ts +7 -0
  5. package/dist/types/collab/guest.d.ts +23 -0
  6. package/dist/types/collab/host.d.ts +29 -0
  7. package/dist/types/collab/protocol.d.ts +113 -0
  8. package/dist/types/collab/relay-client.d.ts +22 -0
  9. package/dist/types/commands/join.d.ts +12 -0
  10. package/dist/types/config/settings-schema.d.ts +60 -5
  11. package/dist/types/export/custom-share.d.ts +1 -2
  12. package/dist/types/export/html/index.d.ts +39 -1
  13. package/dist/types/export/share.d.ts +43 -0
  14. package/dist/types/extensibility/slash-commands.d.ts +1 -11
  15. package/dist/types/main.d.ts +2 -0
  16. package/dist/types/modes/components/agent-hub.d.ts +32 -1
  17. package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
  18. package/dist/types/modes/components/hook-selector.d.ts +4 -6
  19. package/dist/types/modes/components/segment-track.d.ts +11 -6
  20. package/dist/types/modes/components/status-line/component.d.ts +10 -2
  21. package/dist/types/modes/components/status-line/types.d.ts +11 -0
  22. package/dist/types/modes/controllers/event-controller.d.ts +7 -0
  23. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  24. package/dist/types/modes/controllers/session-focus-controller.d.ts +31 -0
  25. package/dist/types/modes/interactive-mode.d.ts +16 -0
  26. package/dist/types/modes/session-observer-registry.d.ts +7 -0
  27. package/dist/types/modes/theme/theme.d.ts +2 -1
  28. package/dist/types/modes/types.d.ts +20 -0
  29. package/dist/types/session/agent-session.d.ts +13 -0
  30. package/dist/types/session/codex-auto-reset.d.ts +8 -4
  31. package/dist/types/session/session-manager.d.ts +21 -0
  32. package/dist/types/session/snapcompact-inline.d.ts +6 -3
  33. package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
  34. package/dist/types/task/executor.d.ts +7 -0
  35. package/dist/types/task/types.d.ts +9 -0
  36. package/package.json +14 -13
  37. package/scripts/bench-guard.ts +71 -0
  38. package/scripts/build-binary.ts +4 -0
  39. package/scripts/bundle-dist.ts +4 -0
  40. package/scripts/generate-share-viewer.ts +34 -0
  41. package/src/cli/args.ts +2 -0
  42. package/src/cli-commands.ts +1 -0
  43. package/src/collab/crypto.ts +63 -0
  44. package/src/collab/guest.ts +450 -0
  45. package/src/collab/host.ts +556 -0
  46. package/src/collab/protocol.ts +232 -0
  47. package/src/collab/relay-client.ts +216 -0
  48. package/src/commands/join.ts +39 -0
  49. package/src/config/model-registry.ts +22 -14
  50. package/src/config/settings-schema.ts +67 -5
  51. package/src/config/settings.ts +12 -0
  52. package/src/export/custom-share.ts +1 -1
  53. package/src/export/html/index.ts +122 -17
  54. package/src/export/html/share-loader.js +102 -0
  55. package/src/export/html/template.css +745 -459
  56. package/src/export/html/template.html +6 -3
  57. package/src/export/html/template.js +240 -915
  58. package/src/export/html/tool-views.generated.js +38 -0
  59. package/src/export/share.ts +268 -0
  60. package/src/extensibility/slash-commands.ts +1 -97
  61. package/src/internal-urls/docs-index.generated.ts +74 -73
  62. package/src/main.ts +33 -11
  63. package/src/modes/components/agent-hub.ts +659 -431
  64. package/src/modes/components/assistant-message.ts +126 -6
  65. package/src/modes/components/collab-prompt-message.ts +30 -0
  66. package/src/modes/components/hook-selector.ts +4 -5
  67. package/src/modes/components/segment-track.ts +44 -7
  68. package/src/modes/components/status-line/component.ts +59 -6
  69. package/src/modes/components/status-line/presets.ts +1 -1
  70. package/src/modes/components/status-line/segments.ts +18 -1
  71. package/src/modes/components/status-line/types.ts +12 -0
  72. package/src/modes/components/tips.txt +4 -1
  73. package/src/modes/controllers/command-controller.ts +55 -96
  74. package/src/modes/controllers/event-controller.ts +45 -16
  75. package/src/modes/controllers/input-controller.ts +175 -9
  76. package/src/modes/controllers/selector-controller.ts +13 -15
  77. package/src/modes/controllers/session-focus-controller.ts +112 -0
  78. package/src/modes/controllers/streaming-reveal.ts +7 -0
  79. package/src/modes/interactive-mode.ts +56 -6
  80. package/src/modes/session-observer-registry.ts +11 -0
  81. package/src/modes/theme/theme.ts +6 -0
  82. package/src/modes/types.ts +20 -0
  83. package/src/modes/utils/ui-helpers.ts +23 -13
  84. package/src/prompts/tools/job.md +1 -1
  85. package/src/sdk.ts +239 -36
  86. package/src/session/agent-session.ts +82 -7
  87. package/src/session/codex-auto-reset.ts +23 -11
  88. package/src/session/session-manager.ts +44 -0
  89. package/src/session/snapcompact-inline.ts +9 -3
  90. package/src/slash-commands/builtin-registry.ts +261 -24
  91. package/src/task/executor.ts +14 -0
  92. package/src/task/index.ts +5 -1
  93. package/src/task/render.ts +76 -5
  94. package/src/task/types.ts +9 -0
  95. package/src/tiny/worker.ts +17 -95
  96. package/src/tools/job.ts +6 -9
  97. package/src/tools/read.ts +38 -5
  98. package/src/tools/write.ts +13 -42
  99. package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
  100. package/dist/types/export/html/template.generated.d.ts +0 -1
  101. package/dist/types/export/html/template.macro.d.ts +0 -5
  102. package/dist/types/tiny/compiled-runtime.d.ts +0 -35
  103. package/scripts/generate-template.ts +0 -33
  104. package/src/bun-imports.d.ts +0 -28
  105. package/src/export/html/template.generated.ts +0 -2
  106. package/src/export/html/template.macro.ts +0 -25
  107. package/src/tiny/compiled-runtime.ts +0 -179
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Session sharing.
3
+ *
4
+ * The session JSON is gzipped and sealed with a fresh AES-256-GCM key
5
+ * (`[12B IV][ciphertext+tag]`, same layout as collab frames), then pushed to
6
+ * one of two stores:
7
+ *
8
+ * 1. A secret GitHub gist (preferred — free, durable, no relay storage)
9
+ * holding base64 of the sealed blob, when an authenticated `gh` exists.
10
+ * 2. The share server (`POST <serverUrl>` → `{"id":"…"}`), capped at 1 MB;
11
+ * oversized sessions are truncated (images first, then long strings,
12
+ * then oldest entries) until the sealed blob fits.
13
+ *
14
+ * Either way the link is `<serverUrl>/<id>#<base64url key>`. The viewer page
15
+ * served there fetches the blob (gist ids are hex; server ids never are),
16
+ * decrypts with the fragment key — which never leaves the browser — and
17
+ * renders the same template as `/export`.
18
+ */
19
+ import * as fs from "node:fs/promises";
20
+ import * as os from "node:os";
21
+ import * as path from "node:path";
22
+ import type { AgentState } from "@oh-my-pi/pi-agent-core";
23
+ import { $which, logger } from "@oh-my-pi/pi-utils";
24
+ import { DEFAULT_SHARE_URL } from "@oh-my-pi/pi-wire";
25
+ import { $ } from "bun";
26
+ import type { SecretObfuscator } from "../secrets/obfuscator";
27
+ import type { SessionManager } from "../session/session-manager";
28
+ import { buildSessionData, type SessionData } from "./html";
29
+
30
+ export { DEFAULT_SHARE_URL };
31
+
32
+ /** Hard cap for blobs accepted by the share server (mirrors relay shareMaxBytes). */
33
+ export const SERVER_MAX_SEALED_BYTES = 1_000_000;
34
+ /** Gist raw fetches cap at 10 MB; keep base64 (×4/3) comfortably under it. */
35
+ const GIST_MAX_SEALED_BYTES = 5_000_000;
36
+
37
+ const IV_LENGTH = 12;
38
+ const SHARE_KEY_BYTES = 32;
39
+ /** The viewer picks the gist file by this suffix. */
40
+ const GIST_FILENAME = "session.ompshare.txt";
41
+ /** Gist ids are hex; the relay never issues pure-hex ids, so the viewer can route on shape. */
42
+ const GIST_ID_RE = /^[0-9a-f]{20,64}$/;
43
+
44
+ /** Progressively harsher per-string caps applied when the sealed blob is over budget. */
45
+ const TEXT_CAPS = [32_768, 8_192, 2_048, 512];
46
+ /** 1×1 transparent GIF; stands in for stripped data-URL images so <img> tags stay valid. */
47
+ const BLANK_IMAGE_DATA_URL = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
48
+ const IMAGE_OMITTED_TEXT = "[image omitted from share]";
49
+
50
+ export interface ShareSessionOptions {
51
+ /** Share server/viewer base URL; defaults to {@link DEFAULT_SHARE_URL}. */
52
+ serverUrl?: string;
53
+ /** Agent state for system prompt + tool descriptions in the snapshot. */
54
+ state?: AgentState;
55
+ /**
56
+ * Redacts the snapshot before sealing: deep-walks every string (entries,
57
+ * header, system prompt, tool descriptions) through the obfuscator, so
58
+ * secrets that landed in persisted entries (tool outputs reading .env,
59
+ * etc.) never leave the machine. Pass undefined to skip.
60
+ */
61
+ obfuscator?: SecretObfuscator;
62
+ }
63
+
64
+ export interface ShareSessionResult {
65
+ /** Viewer link: `<serverUrl>/<id>#<key>`. */
66
+ url: string;
67
+ method: "gist" | "server";
68
+ /** Underlying gist URL (gist method only). */
69
+ gistUrl?: string;
70
+ /** True when content was trimmed to fit the upload budget. */
71
+ truncated: boolean;
72
+ sealedBytes: number;
73
+ }
74
+
75
+ /** Build the snapshot that gets sealed and uploaded, redacted when an obfuscator is provided. */
76
+ export function buildShareSnapshot(sm: SessionManager, options?: ShareSessionOptions): SessionData {
77
+ const data = buildSessionData(sm, options?.state);
78
+ return options?.obfuscator?.hasSecrets() ? options.obfuscator.obfuscateObject(data) : data;
79
+ }
80
+
81
+ /** Share the session; tries a secret gist first, then the share server. */
82
+ export async function shareSession(sm: SessionManager, options?: ShareSessionOptions): Promise<ShareSessionResult> {
83
+ const data = buildShareSnapshot(sm, options);
84
+ const keyBytes = new Uint8Array(SHARE_KEY_BYTES);
85
+ crypto.getRandomValues(keyBytes);
86
+ const key = await crypto.subtle.importKey("raw", keyBytes, "AES-GCM", false, ["encrypt"]);
87
+ const keyText = Buffer.from(keyBytes).toString("base64url");
88
+ const base = normalizeShareServerUrl(options?.serverUrl);
89
+
90
+ const forGist = await sealToFit(key, data, GIST_MAX_SEALED_BYTES);
91
+ const gist = await tryCreateGist(forGist.sealed);
92
+ if (gist) {
93
+ return {
94
+ url: `${base}/${gist.id}#${keyText}`,
95
+ method: "gist",
96
+ gistUrl: gist.url,
97
+ truncated: forGist.truncated,
98
+ sealedBytes: forGist.sealed.byteLength,
99
+ };
100
+ }
101
+
102
+ const forServer =
103
+ forGist.sealed.byteLength <= SERVER_MAX_SEALED_BYTES
104
+ ? forGist
105
+ : await sealToFit(key, data, SERVER_MAX_SEALED_BYTES);
106
+ const id = await uploadToServer(forServer.sealed, base);
107
+ return {
108
+ url: `${base}/${id}#${keyText}`,
109
+ method: "server",
110
+ truncated: forServer.truncated,
111
+ sealedBytes: forServer.sealed.byteLength,
112
+ };
113
+ }
114
+
115
+ /** Strip trailing slashes so `<base>/<id>` composes cleanly. */
116
+ export function normalizeShareServerUrl(serverUrl?: string): string {
117
+ const base = (serverUrl ?? DEFAULT_SHARE_URL).trim().replace(/\/+$/, "");
118
+ return base || DEFAULT_SHARE_URL;
119
+ }
120
+
121
+ interface SealedSession {
122
+ sealed: Uint8Array<ArrayBuffer>;
123
+ truncated: boolean;
124
+ }
125
+
126
+ /** Seal `data`, trimming content until the sealed blob fits `maxBytes`. Exported for tests. */
127
+ export async function sealToFit(key: CryptoKey, data: SessionData, maxBytes: number): Promise<SealedSession> {
128
+ let sealed = await sealSessionData(key, data);
129
+ if (sealed.byteLength <= maxBytes) return { sealed, truncated: false };
130
+
131
+ // Work on a deep copy; the caller may re-fit the original at another budget.
132
+ const working = structuredClone(data);
133
+ stripImagePayloads(working);
134
+ sealed = await sealSessionData(key, working);
135
+ if (sealed.byteLength <= maxBytes) return { sealed, truncated: true };
136
+
137
+ for (const cap of TEXT_CAPS) {
138
+ capLongStrings(working, cap);
139
+ sealed = await sealSessionData(key, working);
140
+ if (sealed.byteLength <= maxBytes) return { sealed, truncated: true };
141
+ }
142
+
143
+ // Last resort: drop oldest entries (orphaned children render as roots).
144
+ while (working.entries.length > 4) {
145
+ working.entries = working.entries.slice(Math.ceil(working.entries.length / 2));
146
+ sealed = await sealSessionData(key, working);
147
+ if (sealed.byteLength <= maxBytes) return { sealed, truncated: true };
148
+ }
149
+
150
+ throw new Error(`Session too large to share: ${sealed.byteLength} bytes sealed exceeds the ${maxBytes} byte limit`);
151
+ }
152
+
153
+ /** `[12B IV][AES-256-GCM(gzip(JSON))]` — decrypted and gunzipped by share-loader.js. */
154
+ async function sealSessionData(key: CryptoKey, data: SessionData): Promise<Uint8Array<ArrayBuffer>> {
155
+ const compressed = Bun.gzipSync(new TextEncoder().encode(JSON.stringify(data)));
156
+ const iv = new Uint8Array(IV_LENGTH);
157
+ crypto.getRandomValues(iv);
158
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, compressed));
159
+ const out = new Uint8Array(IV_LENGTH + ciphertext.byteLength);
160
+ out.set(iv, 0);
161
+ out.set(ciphertext, IV_LENGTH);
162
+ return out;
163
+ }
164
+
165
+ function isRecord(value: unknown): value is Record<string, unknown> {
166
+ return typeof value === "object" && value !== null;
167
+ }
168
+
169
+ /** Replace inline image payloads (image blocks + data: URLs) with tiny placeholders, in place. */
170
+ function stripImagePayloads(value: unknown): void {
171
+ if (Array.isArray(value)) {
172
+ for (let i = 0; i < value.length; i++) {
173
+ const item: unknown = value[i];
174
+ if (isRecord(item) && item.type === "image" && typeof item.data === "string" && item.data.length > 1024) {
175
+ value[i] = { type: "text", text: IMAGE_OMITTED_TEXT };
176
+ continue;
177
+ }
178
+ stripImagePayloads(item);
179
+ }
180
+ return;
181
+ }
182
+ if (!isRecord(value)) return;
183
+ for (const k in value) {
184
+ const v = value[k];
185
+ if (typeof v === "string") {
186
+ if (v.length > 1024 && v.startsWith("data:")) value[k] = BLANK_IMAGE_DATA_URL;
187
+ continue;
188
+ }
189
+ stripImagePayloads(v);
190
+ }
191
+ }
192
+
193
+ /** Truncate every string longer than `cap`, in place. */
194
+ function capLongStrings(value: unknown, cap: number): void {
195
+ if (Array.isArray(value)) {
196
+ for (let i = 0; i < value.length; i++) {
197
+ const item: unknown = value[i];
198
+ if (typeof item === "string" && item.length > cap) value[i] = `${item.slice(0, cap)}\n…[truncated for share]`;
199
+ else capLongStrings(item, cap);
200
+ }
201
+ return;
202
+ }
203
+ if (!isRecord(value)) return;
204
+ for (const k in value) {
205
+ const v = value[k];
206
+ if (typeof v === "string") {
207
+ if (v.length > cap) value[k] = `${v.slice(0, cap)}\n…[truncated for share]`;
208
+ continue;
209
+ }
210
+ capLongStrings(v, cap);
211
+ }
212
+ }
213
+
214
+ /** Create a secret gist holding base64 of the sealed blob; null when `gh` is unusable. */
215
+ async function tryCreateGist(sealed: Uint8Array): Promise<{ id: string; url: string } | null> {
216
+ if (!$which("gh")) return null;
217
+ const auth = await $`gh auth status`.quiet().nothrow();
218
+ if (auth.exitCode !== 0) {
219
+ logger.debug("share: gh present but not authenticated; falling back to share server");
220
+ return null;
221
+ }
222
+
223
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-share-"));
224
+ try {
225
+ const file = path.join(dir, GIST_FILENAME);
226
+ await Bun.write(file, Buffer.from(sealed).toString("base64"));
227
+ const result = await $`gh gist create --public=false ${file}`.quiet().nothrow();
228
+ if (result.exitCode !== 0) {
229
+ logger.warn("share: gist creation failed; falling back to share server", {
230
+ stderr: result.stderr.toString("utf-8").trim().slice(0, 500),
231
+ });
232
+ return null;
233
+ }
234
+ const url = result.text().trim().split("\n").pop()?.trim() ?? "";
235
+ const id = url.split("/").pop() ?? "";
236
+ if (!GIST_ID_RE.test(id)) {
237
+ logger.warn("share: could not parse gist id from gh output", { url });
238
+ return null;
239
+ }
240
+ return { id, url };
241
+ } finally {
242
+ await fs.rm(dir, { recursive: true, force: true });
243
+ }
244
+ }
245
+
246
+ /** POST the sealed blob to the share server; returns the assigned id. */
247
+ async function uploadToServer(sealed: Uint8Array, base: string): Promise<string> {
248
+ let res: Response;
249
+ try {
250
+ res = await fetch(base, {
251
+ method: "POST",
252
+ headers: { "Content-Type": "application/octet-stream" },
253
+ body: sealed,
254
+ });
255
+ } catch (err) {
256
+ throw new Error(`Share upload to ${base} failed: ${err instanceof Error ? err.message : String(err)}`);
257
+ }
258
+ if (!res.ok) {
259
+ const detail = (await res.text().catch(() => "")).trim().slice(0, 200);
260
+ throw new Error(`Share upload to ${base} failed: HTTP ${res.status}${detail ? ` (${detail})` : ""}`);
261
+ }
262
+ const body = (await res.json().catch(() => null)) as { id?: unknown } | null;
263
+ const id = body && typeof body.id === "string" ? body.id : "";
264
+ if (!/^[A-Za-z0-9_-]{10,64}$/.test(id)) {
265
+ throw new Error(`Share upload to ${base} failed: server returned no usable id`);
266
+ }
267
+ return id;
268
+ }
@@ -1,14 +1,8 @@
1
- import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
2
1
  import { parseFrontmatter, prompt } from "@oh-my-pi/pi-utils";
3
2
  import { slashCommandCapability } from "../capability/slash-command";
4
3
  import { appendInlineArgsFallback, templateUsesInlineArgPlaceholders } from "../config/prompt-templates";
5
4
  import type { SlashCommand } from "../discovery";
6
5
  import { loadCapability } from "../discovery";
7
- import {
8
- BUILTIN_SLASH_COMMAND_DEFS,
9
- type BuiltinSlashCommand,
10
- type SubcommandDef,
11
- } from "../slash-commands/builtin-registry";
12
6
  import { EMBEDDED_COMMAND_TEMPLATES } from "../task/commands";
13
7
  import { parseCommandArgs, substituteArgs } from "../utils/command-args";
14
8
 
@@ -24,97 +18,7 @@ export interface SlashCommandInfo {
24
18
  path?: string;
25
19
  }
26
20
 
27
- export type { BuiltinSlashCommand, SubcommandDef } from "../slash-commands/builtin-registry";
28
-
29
- /**
30
- * Build getArgumentCompletions from declarative subcommand definitions.
31
- * Returns subcommand names filtered by prefix in the dropdown.
32
- */
33
- function buildArgumentCompletions(subcommands: SubcommandDef[]): (prefix: string) => AutocompleteItem[] | null {
34
- return (argumentPrefix: string) => {
35
- if (argumentPrefix.includes(" ")) return null; // past the subcommand
36
- const lower = argumentPrefix.toLowerCase();
37
- const matches = subcommands
38
- .filter(s => s.name.startsWith(lower))
39
- .map(s => ({
40
- value: `${s.name} `,
41
- label: s.name,
42
- description: s.description,
43
- hint: s.usage,
44
- }));
45
- return matches.length > 0 ? matches : null;
46
- };
47
- }
48
-
49
- /**
50
- * Build getInlineHint from declarative subcommand definitions.
51
- * Shows remaining completion + usage as dim ghost text after cursor.
52
- */
53
- function buildSubcommandInlineHint(subcommands: SubcommandDef[]): (argumentText: string) => string | null {
54
- return (argumentText: string) => {
55
- const trimmed = argumentText.trimStart();
56
- const spaceIndex = trimmed.indexOf(" ");
57
-
58
- if (spaceIndex === -1) {
59
- // Still typing subcommand name — show remaining chars + usage
60
- const prefix = trimmed.toLowerCase();
61
- if (prefix.length === 0) return null;
62
- const match = subcommands.find(s => s.name.startsWith(prefix));
63
- if (!match) return null;
64
- const remaining = match.name.slice(prefix.length);
65
- return remaining + (match.usage ? ` ${match.usage}` : "");
66
- }
67
-
68
- // Subcommand typed — show remaining usage params
69
- const subName = trimmed.slice(0, spaceIndex).toLowerCase();
70
- const afterSub = trimmed.slice(spaceIndex + 1);
71
- const sub = subcommands.find(s => s.name === subName);
72
- if (!sub?.usage) return null;
73
-
74
- if (afterSub.length > 0) {
75
- const usageParts = sub.usage.split(" ");
76
- const inputParts = afterSub.trim().split(/\s+/);
77
- const remaining = usageParts.slice(inputParts.length);
78
- return remaining.length > 0 ? remaining.join(" ") : null;
79
- }
80
-
81
- return sub.usage;
82
- };
83
- }
84
-
85
- /**
86
- * Build getInlineHint for commands with a simple static hint string.
87
- * Shows the hint only when no arguments have been typed yet.
88
- */
89
- function buildStaticInlineHint(hint: string): (argumentText: string) => string | null {
90
- return (argumentText: string) => (argumentText.trim().length === 0 ? hint : null);
91
- }
92
-
93
- /**
94
- * Materialized builtin slash commands with completion functions derived from
95
- * declarative subcommand/hint definitions.
96
- */
97
- export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<
98
- BuiltinSlashCommand & {
99
- getArgumentCompletions?: (prefix: string) => AutocompleteItem[] | null;
100
- getInlineHint?: (argumentText: string) => string | null;
101
- }
102
- > = BUILTIN_SLASH_COMMAND_DEFS.map(cmd => {
103
- if (cmd.subcommands) {
104
- return {
105
- ...cmd,
106
- getArgumentCompletions: buildArgumentCompletions(cmd.subcommands),
107
- getInlineHint: buildSubcommandInlineHint(cmd.subcommands),
108
- };
109
- }
110
- if (cmd.inlineHint) {
111
- return {
112
- ...cmd,
113
- getInlineHint: buildStaticInlineHint(cmd.inlineHint),
114
- };
115
- }
116
- return cmd;
117
- });
21
+ export type { BuiltinSlashCommand, SubcommandDef } from "../slash-commands/types";
118
22
 
119
23
  /**
120
24
  * Represents a custom slash command loaded from a file