@kevin5251984/guild 0.2.12

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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/bin/guildd.mjs +20 -0
  3. package/cordis.yml +24 -0
  4. package/package.json +52 -0
  5. package/src/agent-file.ts +125 -0
  6. package/src/browser.ts +668 -0
  7. package/src/catalog/default-bots.ts +263 -0
  8. package/src/catalog/skills.ts +128 -0
  9. package/src/catalog/subagents.ts +70 -0
  10. package/src/chat-parts.ts +71 -0
  11. package/src/cli-args.ts +75 -0
  12. package/src/cli.ts +60 -0
  13. package/src/compact.ts +355 -0
  14. package/src/cordis.d.ts +40 -0
  15. package/src/db.ts +653 -0
  16. package/src/generate.ts +673 -0
  17. package/src/handlers.ts +1623 -0
  18. package/src/harness.ts +326 -0
  19. package/src/host-agents.ts +137 -0
  20. package/src/host-browse.ts +199 -0
  21. package/src/host-skills.ts +150 -0
  22. package/src/image-gen.ts +270 -0
  23. package/src/index.ts +12 -0
  24. package/src/llm.ts +993 -0
  25. package/src/mcp.ts +563 -0
  26. package/src/memory.ts +159 -0
  27. package/src/mention.ts +176 -0
  28. package/src/oauth.ts +1474 -0
  29. package/src/plugins/api.ts +8 -0
  30. package/src/plugins/chat.ts +31 -0
  31. package/src/plugins/harness.ts +77 -0
  32. package/src/plugins/llm.ts +50 -0
  33. package/src/plugins/mcp.ts +58 -0
  34. package/src/plugins/memory.ts +42 -0
  35. package/src/plugins/oauth.ts +47 -0
  36. package/src/plugins/server.ts +126 -0
  37. package/src/plugins/store.ts +29 -0
  38. package/src/plugins/tools.ts +79 -0
  39. package/src/public/buddy.js +432 -0
  40. package/src/public/chat.css +3045 -0
  41. package/src/public/chat.html +5834 -0
  42. package/src/public/favicon-16.png +0 -0
  43. package/src/public/favicon-16.svg +10 -0
  44. package/src/public/favicon-32.png +0 -0
  45. package/src/public/favicon.ico +0 -0
  46. package/src/public/favicon.svg +13 -0
  47. package/src/public/i18n.js +663 -0
  48. package/src/public/index.html +143 -0
  49. package/src/public/library.html +678 -0
  50. package/src/public/mcp-add.html +126 -0
  51. package/src/public/md.js +332 -0
  52. package/src/public/rpg/inn-street.jpg +0 -0
  53. package/src/public/settings.html +795 -0
  54. package/src/public/skills-add.html +212 -0
  55. package/src/public/studio.html +1181 -0
  56. package/src/public/style.css +1678 -0
  57. package/src/public/subagents-add.html +152 -0
  58. package/src/router.ts +978 -0
  59. package/src/send-budget.ts +52 -0
  60. package/src/server.ts +1 -0
  61. package/src/skill-import.ts +250 -0
  62. package/src/slash.ts +15 -0
  63. package/src/start.ts +103 -0
  64. package/src/store.ts +1208 -0
  65. package/src/subagent.ts +355 -0
  66. package/src/tools.ts +818 -0
  67. package/src/trajectory.ts +339 -0
  68. package/src/usage.ts +111 -0
  69. package/vendor/protocol/package.json +19 -0
  70. package/vendor/protocol/src/index.ts +159 -0
@@ -0,0 +1,52 @@
1
+ /** Grok 4.6 is 500k. Stay under, and count CJK/code denser than char/4. */
2
+ export const SEND_TOKEN_BUDGET = 400_000;
3
+ const SEND_CHARS_PER_TOKEN = 1.5;
4
+ const KEEP_FLOOR = 2;
5
+
6
+ export function estimateSendTokens(text: string): number {
7
+ return Math.ceil(String(text || "").length / SEND_CHARS_PER_TOKEN);
8
+ }
9
+
10
+ function payloadChars(message: unknown): number {
11
+ if (message == null) return 0;
12
+ if (typeof message === "string") return message.length;
13
+ if (typeof message === "object" && message && "content" in message) {
14
+ const content = (message as { content?: unknown }).content;
15
+ if (typeof content === "string") return content.length;
16
+ try {
17
+ return JSON.stringify(content ?? "").length;
18
+ } catch {
19
+ return 0;
20
+ }
21
+ }
22
+ try {
23
+ return JSON.stringify(message).length;
24
+ } catch {
25
+ return 0;
26
+ }
27
+ }
28
+
29
+ function isSafeSendCut(message: unknown): boolean {
30
+ const role = (message as { role?: string } | null)?.role;
31
+ return role === "user" || role === "assistant" || role === "system";
32
+ }
33
+
34
+ /** Keep a suffix of messages that fits under budget. Drops oldest tool noise first. */
35
+ export function trimSendMessages<T>(
36
+ messages: T[],
37
+ extraTokens = 0,
38
+ budget = SEND_TOKEN_BUDGET,
39
+ ): T[] {
40
+ if (messages.length <= KEEP_FLOOR) return messages;
41
+ let used = extraTokens;
42
+ const kept: T[] = [];
43
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
44
+ const cost = Math.ceil(payloadChars(messages[i]) / SEND_CHARS_PER_TOKEN) + 16;
45
+ if (kept.length >= KEEP_FLOOR && used + cost > budget) break;
46
+ used += cost;
47
+ kept.push(messages[i]);
48
+ }
49
+ const out = kept.reverse();
50
+ while (out.length > KEEP_FLOOR && !isSafeSendCut(out[0])) out.shift();
51
+ return out;
52
+ }
package/src/server.ts ADDED
@@ -0,0 +1 @@
1
+ export { listenGuildServer } from "./plugins/server.ts";
@@ -0,0 +1,250 @@
1
+ import { StoreError } from "./store.ts";
2
+
3
+ export type ImportedSkill = {
4
+ name: string;
5
+ slug?: string;
6
+ description: string;
7
+ body: string;
8
+ sourceUrl?: string;
9
+ };
10
+
11
+ const MAX_BYTES = 200_000;
12
+
13
+ function unquoteYaml(value: string): string {
14
+ const s = value.trim();
15
+ if (
16
+ (s.startsWith('"') && s.endsWith('"')) ||
17
+ (s.startsWith("'") && s.endsWith("'"))
18
+ ) {
19
+ return s.slice(1, -1).trim();
20
+ }
21
+ return s;
22
+ }
23
+
24
+ function yamlField(front: string, key: string): string {
25
+ const lines = front.split(/\r?\n/);
26
+ const start = lines.findIndex((line) =>
27
+ new RegExp(`^${key}:\\s*`).test(line),
28
+ );
29
+ if (start < 0) return "";
30
+ const after = lines[start].replace(new RegExp(`^${key}:\\s*`), "").trim();
31
+ const block = after === ">" || after === ">-" || after === "|" || after === "|-";
32
+ if (block) {
33
+ const folded = after.startsWith(">");
34
+ const collected: string[] = [];
35
+ let base: number | null = null;
36
+ for (let i = start + 1; i < lines.length; i += 1) {
37
+ const line = lines[i];
38
+ if (line.trim() === "") {
39
+ collected.push("");
40
+ continue;
41
+ }
42
+ const indent = (line.match(/^(\s*)/)?.[1] || "").length;
43
+ if (indent === 0) break;
44
+ if (base === null) base = indent;
45
+ if (indent < base) break;
46
+ collected.push(line.slice(base));
47
+ }
48
+ const text = folded
49
+ ? collected
50
+ .join("\n")
51
+ .split(/\n{2,}/)
52
+ .map((para) => para.split(/\n/).map((row) => row.trim()).join(" "))
53
+ .join(" ")
54
+ .replace(/\s+/g, " ")
55
+ : collected.join("\n");
56
+ return text.trim();
57
+ }
58
+ return unquoteYaml(after);
59
+ }
60
+
61
+ export function parseSkillMarkdown(
62
+ text: string,
63
+ fallbackName = "Imported skill",
64
+ ): ImportedSkill {
65
+ const trimmed = text.replace(/^\uFEFF/, "");
66
+ let name = fallbackName;
67
+ let description = "";
68
+ let body = trimmed;
69
+ const fence = trimmed.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
70
+ if (fence) {
71
+ const front = fence[1];
72
+ body = fence[2].trim();
73
+ name = yamlField(front, "name") || name;
74
+ description = yamlField(front, "description");
75
+ }
76
+ if (!body.trim()) {
77
+ throw new StoreError(400, "SKILL.md has no body");
78
+ }
79
+ const heading = body.match(/^#\s+(.+)$/m);
80
+ if (name === fallbackName && heading) name = heading[1].trim();
81
+ if (!description) {
82
+ description = body.split("\n").find((line) => line.trim() && !line.startsWith("#"))?.trim() ?? "";
83
+ }
84
+ return { name, description, body: body.trim() };
85
+ }
86
+
87
+ export function parseGithubRef(input: string): {
88
+ owner: string;
89
+ repo: string;
90
+ ref?: string;
91
+ path?: string;
92
+ file?: string;
93
+ } {
94
+ const raw = input.trim();
95
+ if (!raw) throw new StoreError(400, "repo is required");
96
+
97
+ const blob = raw.match(
98
+ /github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)/i,
99
+ );
100
+ if (blob) {
101
+ return {
102
+ owner: blob[1],
103
+ repo: blob[2].replace(/\.git$/, ""),
104
+ ref: blob[3],
105
+ file: blob[4],
106
+ };
107
+ }
108
+ const tree = raw.match(
109
+ /github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)(?:\/(.*))?/i,
110
+ );
111
+ if (tree) {
112
+ return {
113
+ owner: tree[1],
114
+ repo: tree[2].replace(/\.git$/, ""),
115
+ ref: tree[3],
116
+ path: tree[4] || undefined,
117
+ };
118
+ }
119
+ const repoUrl = raw.match(/github\.com\/([^/]+)\/([^/#?]+)/i);
120
+ if (repoUrl) {
121
+ return { owner: repoUrl[1], repo: repoUrl[2].replace(/\.git$/, "") };
122
+ }
123
+ const short = raw.match(/^([^/\s]+)\/([^/\s]+)(?:\/(.*))?$/);
124
+ if (short && !raw.includes("://")) {
125
+ return {
126
+ owner: short[1],
127
+ repo: short[2].replace(/\.git$/, ""),
128
+ path: short[3] || undefined,
129
+ };
130
+ }
131
+ throw new StoreError(400, "unrecognized GitHub repo");
132
+ }
133
+
134
+ function assertHttpUrl(url: string): URL {
135
+ let parsed: URL;
136
+ try {
137
+ parsed = new URL(url);
138
+ } catch {
139
+ throw new StoreError(400, "invalid URL");
140
+ }
141
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
142
+ throw new StoreError(400, "only http/https URLs are allowed");
143
+ }
144
+ return parsed;
145
+ }
146
+
147
+ export function githubRawUrl(
148
+ owner: string,
149
+ repo: string,
150
+ ref: string,
151
+ filePath: string,
152
+ ): string {
153
+ return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filePath}`;
154
+ }
155
+
156
+ export async function fetchText(
157
+ url: string,
158
+ fetchImpl: typeof fetch = fetch,
159
+ ): Promise<string> {
160
+ assertHttpUrl(url);
161
+ const response = await fetchImpl(url, {
162
+ redirect: "follow",
163
+ signal: AbortSignal.timeout(15_000),
164
+ headers: { "user-agent": "guildd-skill-import" },
165
+ });
166
+ if (!response.ok) {
167
+ throw new StoreError(400, `fetch failed: ${response.status} ${url}`);
168
+ }
169
+ const buf = Buffer.from(await response.arrayBuffer());
170
+ if (buf.length > MAX_BYTES) {
171
+ throw new StoreError(400, "file too large");
172
+ }
173
+ return buf.toString("utf8");
174
+ }
175
+
176
+ export async function importFromUrl(
177
+ url: string,
178
+ fetchImpl: typeof fetch = fetch,
179
+ ): Promise<ImportedSkill[]> {
180
+ const parsed = assertHttpUrl(url);
181
+ if (parsed.hostname === "github.com" && parsed.pathname.includes("/blob/")) {
182
+ const ref = parseGithubRef(url);
183
+ if (ref.file) {
184
+ const raw = githubRawUrl(ref.owner, ref.repo, ref.ref ?? "HEAD", ref.file);
185
+ const text = await fetchText(raw, fetchImpl);
186
+ return [
187
+ {
188
+ ...parseSkillMarkdown(text, ref.file),
189
+ sourceUrl: raw,
190
+ },
191
+ ];
192
+ }
193
+ }
194
+ const text = await fetchText(url, fetchImpl);
195
+ const looksHtml = /^\s*</.test(text) && /<html/i.test(text);
196
+ if (looksHtml) {
197
+ throw new StoreError(
198
+ 400,
199
+ "that URL is an HTML page; paste a SKILL.md or GitHub repo instead",
200
+ );
201
+ }
202
+ return [{ ...parseSkillMarkdown(text), sourceUrl: url }];
203
+ }
204
+
205
+ type GitTree = {
206
+ tree?: { path?: string; type?: string }[];
207
+ };
208
+
209
+ export async function importFromGithub(
210
+ repo: string,
211
+ fetchImpl: typeof fetch = fetch,
212
+ ): Promise<ImportedSkill[]> {
213
+ const ref = parseGithubRef(repo);
214
+ if (ref.file?.toLowerCase().endsWith("skill.md")) {
215
+ const raw = githubRawUrl(ref.owner, ref.repo, ref.ref ?? "HEAD", ref.file);
216
+ const text = await fetchText(raw, fetchImpl);
217
+ return [{ ...parseSkillMarkdown(text, ref.file), sourceUrl: raw }];
218
+ }
219
+
220
+ const branch = ref.ref ?? "HEAD";
221
+ const treeUrl = `https://api.github.com/repos/${ref.owner}/${ref.repo}/git/trees/${branch}?recursive=1`;
222
+ const treeText = await fetchText(treeUrl, fetchImpl);
223
+ let tree: GitTree;
224
+ try {
225
+ tree = JSON.parse(treeText) as GitTree;
226
+ } catch {
227
+ throw new StoreError(400, "GitHub tree response was not JSON");
228
+ }
229
+ const prefix = ref.path ? `${ref.path.replace(/\/$/, "")}/` : "";
230
+ const files = (tree.tree ?? [])
231
+ .filter((entry) => entry.type === "blob" && entry.path)
232
+ .map((entry) => entry.path as string)
233
+ .filter((path) => path.toLowerCase().endsWith("/skill.md") || path.toLowerCase() === "skill.md")
234
+ .filter((path) => (prefix ? path.startsWith(prefix) : true));
235
+
236
+ if (files.length === 0) {
237
+ throw new StoreError(400, "no SKILL.md found in that repo");
238
+ }
239
+
240
+ const imported: ImportedSkill[] = [];
241
+ for (const filePath of files.slice(0, 40)) {
242
+ const raw = githubRawUrl(ref.owner, ref.repo, branch, filePath);
243
+ const text = await fetchText(raw, fetchImpl);
244
+ imported.push({
245
+ ...parseSkillMarkdown(text, filePath),
246
+ sourceUrl: raw,
247
+ });
248
+ }
249
+ return imported;
250
+ }
package/src/slash.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { stripMentionNoise } from "./mention.ts";
2
+
3
+ /** `/slug` tokens in a user message. Leading slash after start or whitespace. */
4
+ export function slashNames(text: string): string[] {
5
+ const scan = stripMentionNoise(text);
6
+ const out: string[] = [];
7
+ const seen = new Set<string>();
8
+ for (const row of scan.matchAll(/(?:^|\s)\/([A-Za-z0-9_.-]+)/g)) {
9
+ const key = row[1].toLowerCase();
10
+ if (!key || seen.has(key)) continue;
11
+ seen.add(key);
12
+ out.push(key);
13
+ }
14
+ return out;
15
+ }
package/src/start.ts ADDED
@@ -0,0 +1,103 @@
1
+ import type { Server } from "node:http";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
+ import { Context } from "cordis";
4
+ import Loader from "@cordisjs/plugin-loader";
5
+ import Include from "@cordisjs/plugin-include";
6
+ import { DEFAULT_GUILD_PORT } from "@guild/protocol";
7
+ import { envSandbox } from "./harness.ts";
8
+
9
+ Include.prototype.write = function write() {};
10
+
11
+ const DAEMON_DIR = fileURLToPath(new URL("..", import.meta.url));
12
+ const LISTEN_TIMEOUT_MS = 10_000;
13
+
14
+ export type CreateGuildContextOptions = {
15
+ configPath?: string;
16
+ patches?: Array<{
17
+ id: string;
18
+ disabled?: boolean | null;
19
+ config?: unknown;
20
+ }>;
21
+ };
22
+
23
+ export type StartedDaemon = {
24
+ server: Server;
25
+ ctx: Context;
26
+ };
27
+
28
+ const guildEnvs = new WeakMap<Context, NodeJS.ProcessEnv>();
29
+
30
+ export function guildEnvOf(ctx: Context): NodeJS.ProcessEnv {
31
+ return guildEnvs.get(ctx.root) ?? process.env;
32
+ }
33
+
34
+ function parsePort(env: NodeJS.ProcessEnv): number {
35
+ const raw = env.GUILD_PORT ?? String(DEFAULT_GUILD_PORT);
36
+ const port = Number(raw);
37
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
38
+ throw new Error(`invalid GUILD_PORT: ${env.GUILD_PORT}`);
39
+ }
40
+ return port;
41
+ }
42
+
43
+ async function sleep(ms: number): Promise<void> {
44
+ await new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+
47
+ export async function createGuildContext(
48
+ env: NodeJS.ProcessEnv = process.env,
49
+ options: CreateGuildContextOptions = {},
50
+ ): Promise<Context> {
51
+ parsePort(env);
52
+ const ctx = new Context();
53
+ guildEnvs.set(ctx, env);
54
+ let baseUrl = pathToFileURL(DAEMON_DIR).href;
55
+ if (!baseUrl.endsWith("/")) baseUrl += "/";
56
+ ctx.baseUrl = baseUrl;
57
+
58
+ await ctx.plugin(Loader, { baseUrl });
59
+ await ctx.loader.create({
60
+ name: "@cordisjs/plugin-include",
61
+ config: {
62
+ path: options.configPath ?? "./cordis.yml",
63
+ patches: options.patches ?? [],
64
+ },
65
+ });
66
+ await ctx.loader.await();
67
+
68
+ const deadline = Date.now() + LISTEN_TIMEOUT_MS;
69
+ while (Date.now() < deadline) {
70
+ const server = ctx.get("server");
71
+ if (server) {
72
+ const remaining = Math.max(deadline - Date.now(), 0);
73
+ const result = await Promise.race([
74
+ server.whenListening().then((info) => ({ ok: true as const, info })),
75
+ sleep(remaining).then(() => ({ ok: false as const })),
76
+ ]);
77
+ if (!result.ok) throw new Error("guildd did not listen");
78
+ return ctx;
79
+ }
80
+ await sleep(50);
81
+ }
82
+ throw new Error("guildd did not listen");
83
+ }
84
+
85
+ export async function startGuildDaemon(
86
+ env: NodeJS.ProcessEnv = process.env,
87
+ ): Promise<StartedDaemon> {
88
+ const ctx = await createGuildContext(env);
89
+ const harness = ctx.get("harness");
90
+ const line = JSON.stringify({
91
+ listening: true,
92
+ host: ctx.server.host,
93
+ port: ctx.server.port,
94
+ service: "guildd",
95
+ status: "ok",
96
+ ready: true,
97
+ dataDir: ctx.store.dataDir,
98
+ sandbox: envSandbox(env) ?? "position",
99
+ workspace: harness?.workspace(),
100
+ });
101
+ process.stdout.write(`${line}\n`);
102
+ return { server: ctx.server.node, ctx };
103
+ }