@simonepri/refined-antigravity-acp 0.1.0 → 0.1.2

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 (82) hide show
  1. package/dist/bin/cli.d.ts +3 -0
  2. package/dist/bin/cli.d.ts.map +1 -0
  3. package/dist/bin/cli.js +56 -0
  4. package/dist/bin/cli.js.map +1 -0
  5. package/dist/index.server.d.ts +4 -0
  6. package/dist/index.server.d.ts.map +1 -0
  7. package/dist/index.server.js +7 -0
  8. package/dist/index.server.js.map +1 -0
  9. package/dist/src/agy-patch.d.ts +65 -0
  10. package/dist/src/agy-patch.d.ts.map +1 -0
  11. package/dist/src/agy-patch.js +351 -0
  12. package/dist/src/agy-patch.js.map +1 -0
  13. package/dist/src/auth.d.ts +27 -0
  14. package/dist/src/auth.d.ts.map +1 -0
  15. package/dist/src/auth.js +113 -0
  16. package/dist/src/auth.js.map +1 -0
  17. package/dist/src/command.d.ts +5 -0
  18. package/dist/src/command.d.ts.map +1 -0
  19. package/dist/src/command.js +74 -0
  20. package/dist/src/command.js.map +1 -0
  21. package/dist/src/connector.d.ts +188 -0
  22. package/dist/src/connector.d.ts.map +1 -0
  23. package/dist/src/connector.js +1138 -0
  24. package/dist/src/connector.js.map +1 -0
  25. package/dist/src/history.d.ts +12 -0
  26. package/dist/src/history.d.ts.map +1 -0
  27. package/dist/src/history.js +152 -0
  28. package/dist/src/history.js.map +1 -0
  29. package/dist/src/mcp-proxy.d.ts +83 -0
  30. package/dist/src/mcp-proxy.d.ts.map +1 -0
  31. package/dist/src/mcp-proxy.js +308 -0
  32. package/dist/src/mcp-proxy.js.map +1 -0
  33. package/dist/src/provider.d.ts +11 -0
  34. package/dist/src/provider.d.ts.map +1 -0
  35. package/dist/src/provider.js +31 -0
  36. package/dist/src/provider.js.map +1 -0
  37. package/dist/src/sanitize.d.ts +29 -0
  38. package/dist/src/sanitize.d.ts.map +1 -0
  39. package/dist/src/sanitize.js +237 -0
  40. package/dist/src/sanitize.js.map +1 -0
  41. package/dist/src/skills.d.ts +17 -0
  42. package/dist/src/skills.d.ts.map +1 -0
  43. package/dist/src/skills.js +140 -0
  44. package/dist/src/skills.js.map +1 -0
  45. package/dist/src/steering.d.ts +30 -0
  46. package/dist/src/steering.d.ts.map +1 -0
  47. package/dist/src/steering.js +174 -0
  48. package/dist/src/steering.js.map +1 -0
  49. package/package.json +20 -13
  50. package/paseo-plugin.json +0 -1
  51. package/readme.md +3 -5
  52. package/.fallowrc.json +0 -11
  53. package/.github/workflows/ci.yml +0 -24
  54. package/.github/workflows/release.yml +0 -41
  55. package/.oxlintignore +0 -2
  56. package/.oxlintrc.json +0 -152
  57. package/.release-please-manifest.json +0 -3
  58. package/bin/cli.ts +0 -58
  59. package/index.server.ts +0 -9
  60. package/pnpm-workspace.yaml +0 -44
  61. package/release-please-config.json +0 -13
  62. package/server/agy-patch.test.ts +0 -445
  63. package/server/agy-patch.ts +0 -445
  64. package/server/auth.test.ts +0 -51
  65. package/server/auth.ts +0 -122
  66. package/server/command.test.ts +0 -18
  67. package/server/command.ts +0 -92
  68. package/server/connector.test.ts +0 -1136
  69. package/server/connector.ts +0 -1386
  70. package/server/history.test.ts +0 -139
  71. package/server/history.ts +0 -157
  72. package/server/mcp-proxy.test.ts +0 -368
  73. package/server/mcp-proxy.ts +0 -375
  74. package/server/provider.test.ts +0 -11
  75. package/server/provider.ts +0 -37
  76. package/server/sanitize.test.ts +0 -174
  77. package/server/sanitize.ts +0 -286
  78. package/server/skills.test.ts +0 -121
  79. package/server/skills.ts +0 -183
  80. package/server/steering.test.ts +0 -405
  81. package/server/steering.ts +0 -199
  82. package/tsconfig.json +0 -14
@@ -1,286 +0,0 @@
1
- export const HARNESS_TAGS = [
2
- "system_message",
3
- "system_notification",
4
- "system_information",
5
- "system_instruction",
6
- "task_notification",
7
- "task_output",
8
- "context",
9
- "messaging",
10
- "receive_notifications_directly",
11
- "wait_for_task",
12
- "additional_metadata",
13
- "user_request",
14
- "user_information",
15
- "scratchpad",
16
- "identity",
17
- "communication_style",
18
- "mcp_servers",
19
- "subagents",
20
- "artifacts",
21
- "ask_question",
22
- ] as const;
23
-
24
- const HARNESS_SET = new Set(HARNESS_TAGS.map((t) => t.toLowerCase()));
25
-
26
- /**
27
- * Marker prepended to the replacement prompt Paseo dispatches when the user steers
28
- * a running turn. It tells the model the new text interrupts and supersedes the
29
- * instruction it was working on. It is stripped again on every read path:
30
- * assistant output (`sanitizeAssistantText`, `cleanStreamingChunk`) and SQLite
31
- * history replay (`server/history.ts`), so it never reaches the Paseo timeline.
32
- */
33
- export const STEERING_PREFIX = "[Mid-turn update]: ";
34
-
35
- const STEERING_PREFIX_PATTERN = /(?:^|\n)\s*\[Mid-turn update\]:\s*/gi;
36
-
37
- /** Removes any echoed steering marker from text that is about to be displayed. */
38
- export function stripSteeringPrefix(text: string): string {
39
- return text.replace(STEERING_PREFIX_PATTERN, "");
40
- }
41
-
42
- function isHarnessTag(tagName: string): boolean {
43
- return HARNESS_SET.has(tagName.toLowerCase());
44
- }
45
-
46
- function isPossibleHarnessPrefix(str: string): boolean {
47
- const clean = str.replace(/^<\/?/, "").toLowerCase();
48
- if (clean === "") return true;
49
- if (/^[^\sa-zA-Z0-9_-]/.test(clean)) return false;
50
- return HARNESS_TAGS.some((t) => t.toLowerCase().startsWith(clean));
51
- }
52
-
53
- const SYSTEM_MSG_INTRO = "The following is a <SYSTEM_MESSAGE> not actually sent by the user";
54
-
55
- function checkBannerPartialPrefix(input: string): number | null {
56
- const minPrefixLen = 15;
57
- for (let len = Math.min(input.length, SYSTEM_MSG_INTRO.length); len >= minPrefixLen; len--) {
58
- const candidate = input.slice(input.length - len);
59
- if (SYSTEM_MSG_INTRO.toLowerCase().startsWith(candidate.toLowerCase())) {
60
- return input.length - len;
61
- }
62
- }
63
- return null;
64
- }
65
-
66
- function cleanStreamingChunk(text: string): string {
67
- return text
68
- .replace(
69
- /(?:\b(?:The\s+request\s+was\s+cancell?ed\s+by\s+the\s+client|context\s+cancell?ed)\b\.?)/gi,
70
- "",
71
- )
72
- .replace(STEERING_PREFIX_PATTERN, "")
73
- .replace(/\$\s*\\(?:to|rightarrow)\s*\$/gi, "→")
74
- .replace(/\$\s*\\leftarrow\s*\$/gi, "←")
75
- .replace(/\$\s*\\(?:implies|Rightarrow)\s*\$/gi, "⇒")
76
- .replace(/\$\s*\\leftrightarrow\s*\$/gi, "↔")
77
- .replace(/\$\s*\\le(?:q)?\s*\$/gi, "≤")
78
- .replace(/\$\s*\\ge(?:q)?\s*\$/gi, "≥")
79
- .replace(/\$\s*\\ne(?:q)?\s*\$/gi, "≠")
80
- .replace(/\$\s*\\approx\s*\$/gi, "≈")
81
- .replace(/\$\s*\\times\s*\$/gi, "×")
82
- .replace(/\$\s*\\pm\s*\$/gi, "±")
83
- .replace(/\$\s*\\infty\s*\$/gi, "∞")
84
- .replace(/\\(?:to|rightarrow)\b/gi, "→")
85
- .replace(/\\leftarrow\b/gi, "←")
86
- .replace(/<kbd>(.*?)<\/kbd>/gi, "`$1`")
87
- .replace(/<u>(.*?)<\/u>/gi, "*$1*")
88
- .replace(/<mark>(.*?)<\/mark>/gi, "**$1**")
89
- .replace(/<sup>(.*?)<\/sup>/gi, "^($1)");
90
- }
91
-
92
- export class StreamSanitizer {
93
- private inHarnessTag = false;
94
- private buffer = "";
95
-
96
- reset(): void {
97
- this.inHarnessTag = false;
98
- this.buffer = "";
99
- }
100
-
101
- private processInHarness(input: string): string {
102
- const closeMatch = input.match(/<\/\s*([a-zA-Z0-9_-]+)\s*>/i);
103
- if (closeMatch && closeMatch.index !== undefined) {
104
- const tagName = closeMatch[1].toLowerCase();
105
- const endIdx = closeMatch.index + closeMatch[0].length;
106
- if (isHarnessTag(tagName)) {
107
- this.inHarnessTag = false;
108
- }
109
- return input.slice(endIdx);
110
- }
111
-
112
- const lastLt = input.lastIndexOf("<");
113
- if (lastLt !== -1 && lastLt >= input.length - 40) {
114
- const candidate = input.slice(lastLt);
115
- if (isPossibleHarnessPrefix(candidate)) {
116
- this.buffer = candidate;
117
- return "";
118
- }
119
- }
120
- return "";
121
- }
122
-
123
- private handleFullTag(input: string, gtIdx: number): { text: string; remainder: string } {
124
- const fullTag = input.slice(0, gtIdx + 1);
125
- const match = fullTag.match(/^<\s*(\/?)\s*([a-zA-Z0-9_-]+)/);
126
- if (match) {
127
- const isClosing = match[1] === "/";
128
- const tagName = match[2].toLowerCase();
129
- if (isHarnessTag(tagName)) {
130
- if (!isClosing) this.inHarnessTag = true;
131
- return { text: "", remainder: input.slice(gtIdx + 1) };
132
- }
133
- }
134
- return { text: input[0], remainder: input.slice(1) };
135
- }
136
-
137
- process(chunk: string): string {
138
- let input = this.buffer + chunk;
139
- this.buffer = "";
140
- let output = "";
141
-
142
- while (input.length > 0) {
143
- if (this.inHarnessTag) {
144
- input = this.processInHarness(input);
145
- continue;
146
- }
147
-
148
- const sysMatch = input.match(
149
- /The following is a <SYSTEM_MESSAGE> not actually sent by the user[^\n]*\n?/i,
150
- );
151
- if (sysMatch && sysMatch.index !== undefined) {
152
- output += input.slice(0, sysMatch.index);
153
- input = input.slice(sysMatch.index + sysMatch[0].length);
154
- this.inHarnessTag = true;
155
- continue;
156
- }
157
-
158
- const bannerPrefixIdx = checkBannerPartialPrefix(input);
159
- if (bannerPrefixIdx !== null) {
160
- output += input.slice(0, bannerPrefixIdx);
161
- this.buffer = input.slice(bannerPrefixIdx);
162
- break;
163
- }
164
-
165
- const ltIdx = input.indexOf("<");
166
- if (ltIdx === -1) {
167
- output += input;
168
- break;
169
- }
170
-
171
- if (ltIdx > 0) {
172
- output += input.slice(0, ltIdx);
173
- input = input.slice(ltIdx);
174
- }
175
-
176
- const gtIdx = input.indexOf(">");
177
- if (gtIdx === -1) {
178
- if (isPossibleHarnessPrefix(input)) {
179
- this.buffer = input;
180
- break;
181
- }
182
- output += input[0];
183
- input = input.slice(1);
184
- continue;
185
- }
186
-
187
- const res = this.handleFullTag(input, gtIdx);
188
- output += res.text;
189
- input = res.remainder;
190
- }
191
-
192
- return cleanStreamingChunk(output);
193
- }
194
-
195
- flush(): string {
196
- const remaining = this.buffer;
197
- this.buffer = "";
198
- if (this.inHarnessTag) return "";
199
- return cleanStreamingChunk(remaining);
200
- }
201
- }
202
-
203
- export function sanitizeAssistantText(text: string): string {
204
- const harnessPattern = HARNESS_TAGS.join("|");
205
- return (
206
- text
207
- // Strip leaked internal harness tags & system notifications (both paired and streaming/unclosed)
208
- .replace(
209
- /The following is a <SYSTEM_MESSAGE> not actually sent by the user[\s\S]*?<\/SYSTEM_MESSAGE>/gi,
210
- "",
211
- )
212
- .replace(
213
- new RegExp(
214
- `<(?:${harnessPattern})>(?:[\\s\\S]*?<\\/(?:${harnessPattern})>|[\\s\\S]*$)`,
215
- "gi",
216
- ),
217
- "",
218
- )
219
- // Strip stray unclosed/opened harness tags if any remain
220
- .replace(new RegExp(`<\\/?(?:${harnessPattern})>`, "gi"), "")
221
- // Strip client cancellation error messages emitted during turn abort / steering
222
- .replace(
223
- /(?:\b(?:The\s+request\s+was\s+cancell?ed\s+by\s+the\s+client|context\s+cancell?ed)\b\.?)/gi,
224
- "",
225
- )
226
- // Strip any echoed mid-turn update prefixes
227
- .replace(STEERING_PREFIX_PATTERN, "")
228
- // Convert common LaTeX math escapes to Unicode (Paseo does not render KaTeX/LaTeX)
229
- .replace(/\$\s*\\(?:to|rightarrow)\s*\$/gi, "→")
230
- .replace(/\$\s*\\leftarrow\s*\$/gi, "←")
231
- .replace(/\$\s*\\(?:implies|Rightarrow)\s*\$/gi, "⇒")
232
- .replace(/\$\s*\\leftrightarrow\s*\$/gi, "↔")
233
- .replace(/\$\s*\\le(?:q)?\s*\$/gi, "≤")
234
- .replace(/\$\s*\\ge(?:q)?\s*\$/gi, "≥")
235
- .replace(/\$\s*\\ne(?:q)?\s*\$/gi, "≠")
236
- .replace(/\$\s*\\approx\s*\$/gi, "≈")
237
- .replace(/\$\s*\\times\s*\$/gi, "×")
238
- .replace(/\$\s*\\pm\s*\$/gi, "±")
239
- .replace(/\$\s*\\infty\s*\$/gi, "∞")
240
- .replace(/\\(?:to|rightarrow)\b/gi, "→")
241
- .replace(/\\leftarrow\b/gi, "←")
242
- // Normalize keyboard keys for Paseo
243
- .replace(/<kbd>(.*?)<\/kbd>/gi, "`$1`")
244
- // Normalize basic styling tags that Paseo markdown parser does not wrap
245
- .replace(/<u>(.*?)<\/u>/gi, "*$1*")
246
- .replace(/<mark>(.*?)<\/mark>/gi, "**$1**")
247
- .replace(/<sup>(.*?)<\/sup>/gi, "^($1)")
248
- .trim()
249
- );
250
- }
251
-
252
- export function formatSystemContext(systemPrompt?: string): string {
253
- const parts: string[] = [];
254
-
255
- if (systemPrompt && systemPrompt.trim().length > 0) {
256
- parts.push(`[System Context]:\n${systemPrompt.trim()}`);
257
- }
258
-
259
- parts.push(
260
- `[Formatting Guidance]:
261
- - Math: The UI does not render LaTeX ($...$, $$...$$, \\to, \\frac). Use plain text or Unicode formulas (e.g. x^2, a/b, →, √, ∑).
262
- - Mermaid: Always wrap both node labels AND edge labels containing spaces, punctuation, slashes, or parentheses in double quotes (e.g. id["Label (Details)"] and -->|"Label (Details)"|). Never put unquoted parentheses inside edge pipes |...| or node shapes.
263
- - Keys & Markup: Use markdown backticks (\`Cmd\`, \`Ctrl+C\`) instead of HTML <kbd> tags. Never output internal tags like <system_message>.`,
264
- );
265
-
266
- return parts.join("\n\n");
267
- }
268
-
269
- /**
270
- * Formats the replacement prompt for a steered turn, marking it as a mid-turn
271
- * interruption so the model treats it as superseding the instruction in flight.
272
- */
273
- const SYSTEM_CONTEXT_PATTERN =
274
- /(?:\[System Context\]:[\s\S]*?)?\[Formatting Guidance\]:[\s\S]*?(?=\n\n|$)\n*/g;
275
-
276
- /**
277
- * Removes injected system context and formatting guidance from replayed user text so it
278
- * does not clutter the Paseo timeline.
279
- */
280
- export function stripSystemContext(text: string): string {
281
- return text.replace(SYSTEM_CONTEXT_PATTERN, "");
282
- }
283
-
284
- export function formatSteeringPrompt(userText: string): string {
285
- return `${STEERING_PREFIX}${userText.trim()}`;
286
- }
@@ -1,121 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import {
6
- augmentCommands,
7
- discoverSkills,
8
- expandSkillInvocation,
9
- parseSkillFile,
10
- } from "./skills.js";
11
-
12
- describe("skills", () => {
13
- let tmpDir: string;
14
-
15
- beforeEach(() => {
16
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "paseo-test-skills-"));
17
- });
18
-
19
- afterEach(() => {
20
- fs.rmSync(tmpDir, { recursive: true, force: true });
21
- });
22
-
23
- it("parses skill file with YAML frontmatter", () => {
24
- const skillDir = path.join(tmpDir, "my-skill");
25
- fs.mkdirSync(skillDir, { recursive: true });
26
- const skillPath = path.join(skillDir, "SKILL.md");
27
- fs.writeFileSync(
28
- skillPath,
29
- `---
30
- name: my-skill
31
- description: Custom test skill
32
- user-invocable: true
33
- ---
34
-
35
- Instructions here for $ARGUMENTS.`,
36
- );
37
-
38
- const skill = parseSkillFile(skillPath);
39
- expect(skill).not.toBeNull();
40
- expect(skill?.name).toBe("my-skill");
41
- expect(skill?.description).toBe("Custom test skill");
42
- expect(skill?.userInvocable).toBe(true);
43
- expect(skill?.instructions).toBe("Instructions here for $ARGUMENTS.");
44
- });
45
-
46
- it("handles user-invocable: false", () => {
47
- const skillDir = path.join(tmpDir, "internal-skill");
48
- fs.mkdirSync(skillDir, { recursive: true });
49
- const skillPath = path.join(skillDir, "SKILL.md");
50
- fs.writeFileSync(
51
- skillPath,
52
- `---
53
- name: internal-skill
54
- description: Internal skill
55
- user-invocable: false
56
- ---
57
-
58
- Instructions.`,
59
- );
60
-
61
- const skill = parseSkillFile(skillPath);
62
- expect(skill?.userInvocable).toBe(false);
63
- });
64
-
65
- it("discovers skills in workspace directory", () => {
66
- const wsSkills = path.join(tmpDir, ".gemini", "skills", "sample");
67
- fs.mkdirSync(wsSkills, { recursive: true });
68
- fs.writeFileSync(
69
- path.join(wsSkills, "SKILL.md"),
70
- `---
71
- name: sample
72
- description: Sample skill
73
- ---
74
- Do something.`,
75
- );
76
-
77
- const skills = discoverSkills(tmpDir);
78
- expect(skills.has("sample")).toBe(true);
79
- expect(skills.get("sample")?.description).toBe("Sample skill");
80
- });
81
-
82
- it("augments commands list with discovered skills", () => {
83
- const wsSkills = path.join(tmpDir, ".agents", "skills", "test-skill");
84
- fs.mkdirSync(wsSkills, { recursive: true });
85
- fs.writeFileSync(
86
- path.join(wsSkills, "SKILL.md"),
87
- `---
88
- name: test-skill
89
- description: A workspace skill
90
- ---
91
- Do something.`,
92
- );
93
-
94
- const commands = augmentCommands([{ name: "help", description: "Help" }], tmpDir);
95
- expect(commands.some((c) => c.name === "test-skill")).toBe(true);
96
- expect(commands.some((c) => c.name === "help")).toBe(true);
97
- });
98
-
99
- it("expands skill invocation with arguments substitution", () => {
100
- const wsSkills = path.join(tmpDir, ".agents", "skills", "greet");
101
- fs.mkdirSync(wsSkills, { recursive: true });
102
- fs.writeFileSync(
103
- path.join(wsSkills, "SKILL.md"),
104
- `---
105
- name: greet
106
- description: Greet someone
107
- ---
108
- Say hello to $ARGUMENTS now!`,
109
- );
110
-
111
- const expanded = expandSkillInvocation("/greet Alice", tmpDir);
112
- expect(expanded).toContain("[Skill: greet]");
113
- expect(expanded).toContain("Say hello to Alice now!");
114
- expect(expanded).toContain("[/Skill: greet]");
115
- });
116
-
117
- it("leaves non-skill commands untouched", () => {
118
- const text = "/unknown-command foo bar";
119
- expect(expandSkillInvocation(text, tmpDir)).toBe(text);
120
- });
121
- });
package/server/skills.ts DELETED
@@ -1,183 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
-
5
- export interface AvailableCommand {
6
- name: string;
7
- description: string;
8
- }
9
-
10
- export interface DiscoveredSkill {
11
- name: string;
12
- description: string;
13
- instructions: string;
14
- userInvocable: boolean;
15
- filePath: string;
16
- }
17
-
18
- function unquote(val: string): string {
19
- const trimmed = val.trim();
20
- if (
21
- (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
22
- (trimmed.startsWith("'") && trimmed.endsWith("'"))
23
- ) {
24
- return trimmed.slice(1, -1).replace(/''/g, "'");
25
- }
26
- const commentIdx = trimmed.search(/\s+#/);
27
- return commentIdx !== -1 ? trimmed.slice(0, commentIdx).trim() : trimmed;
28
- }
29
-
30
- function parseInvocable(val: string): boolean {
31
- const v = unquote(val).toLowerCase();
32
- return v !== "false" && v !== "no" && v !== "0";
33
- }
34
-
35
- function parseDescription(
36
- rest: string,
37
- lines: string[],
38
- i: number,
39
- ): { desc: string; nextIndex: number } {
40
- if (rest === ">" || rest === ">-" || rest === "|" || rest === "|-") {
41
- const block: string[] = [];
42
- let idx = i + 1;
43
- while (idx < lines.length && (/^\s+/.test(lines[idx]) || lines[idx].trim().length === 0)) {
44
- block.push(lines[idx].trim());
45
- idx++;
46
- }
47
- const text = rest.startsWith(">") ? block.join(" ") : block.join("\n").trim();
48
- return { desc: text, nextIndex: idx - 1 };
49
- }
50
- return { desc: unquote(rest), nextIndex: i };
51
- }
52
-
53
- function parseFrontmatter(frontmatter: string): {
54
- name?: string;
55
- description?: string;
56
- userInvocable: boolean;
57
- } {
58
- const lines = frontmatter.split(/\r?\n/);
59
- let name: string | undefined;
60
- let description: string | undefined;
61
- let userInvocable = true;
62
-
63
- for (let i = 0; i < lines.length; i++) {
64
- const line = lines[i];
65
- const nameMatch = line.match(/^name:\s*(.*)$/);
66
- if (nameMatch) name = unquote(nameMatch[1]);
67
-
68
- const invocableMatch = line.match(/^(?:user-invocable|user_invocable):\s*(.*)$/);
69
- if (invocableMatch) userInvocable = parseInvocable(invocableMatch[1]);
70
-
71
- const descMatch = line.match(/^description:\s*(.*)$/);
72
- if (descMatch) {
73
- const parsed = parseDescription(descMatch[1].trim(), lines, i);
74
- description = parsed.desc;
75
- i = parsed.nextIndex;
76
- }
77
- }
78
- return { name, description, userInvocable };
79
- }
80
-
81
- export function parseSkillFile(filePath: string): DiscoveredSkill | null {
82
- if (!existsSync(filePath)) return null;
83
- try {
84
- const content = readFileSync(filePath, "utf-8");
85
- const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)(?:\r?\n|$)([\s\S]*)$/);
86
- const meta = fmMatch ? parseFrontmatter(fmMatch[1]) : { userInvocable: true };
87
- const instructions = fmMatch ? fmMatch[2].trim() : content;
88
- const fallbackName = path.basename(path.dirname(filePath));
89
-
90
- return {
91
- name: meta.name && meta.name.length > 0 ? meta.name : fallbackName,
92
- description:
93
- meta.description && meta.description.length > 0
94
- ? meta.description
95
- : `Skill: ${fallbackName}`,
96
- instructions,
97
- userInvocable: meta.userInvocable,
98
- filePath,
99
- };
100
- } catch {
101
- return null;
102
- }
103
- }
104
-
105
- export function resolveSkillRoots(cwd?: string): string[] {
106
- const candidates: string[] = [];
107
- if (cwd) {
108
- candidates.push(
109
- path.join(cwd, ".agents/skills"),
110
- path.join(cwd, ".gemini/skills"),
111
- path.join(cwd, ".codex/skills"),
112
- path.join(cwd, "skills"),
113
- );
114
- }
115
- const home = os.homedir();
116
- candidates.push(
117
- path.join(home, ".gemini/antigravity-cli/skills"),
118
- path.join(home, ".gemini/config/skills"),
119
- path.join(home, ".gemini/skills"),
120
- path.join(home, ".agents/skills"),
121
- path.join(home, ".claude/skills"),
122
- path.join(home, ".codex/skills"),
123
- );
124
- return Array.from(new Set(candidates.filter((dir) => existsSync(dir))));
125
- }
126
-
127
- export function discoverSkills(cwd?: string): Map<string, DiscoveredSkill> {
128
- const roots = resolveSkillRoots(cwd);
129
- const skills = new Map<string, DiscoveredSkill>();
130
-
131
- for (const root of roots) {
132
- try {
133
- for (const entry of readdirSync(root, { withFileTypes: true })) {
134
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
135
- const skill = parseSkillFile(path.join(root, entry.name, "SKILL.md"));
136
- if (skill && !skills.has(skill.name)) {
137
- skills.set(skill.name, skill);
138
- }
139
- }
140
- } catch {
141
- // Ignore unreadable dirs
142
- }
143
- }
144
- return skills;
145
- }
146
-
147
- export function augmentCommands(
148
- existingCommands: AvailableCommand[] | undefined,
149
- cwd?: string,
150
- ): AvailableCommand[] {
151
- const result = new Map<string, AvailableCommand>();
152
- if (Array.isArray(existingCommands)) {
153
- for (const cmd of existingCommands) {
154
- if (cmd?.name) result.set(cmd.name, cmd);
155
- }
156
- }
157
- for (const skill of discoverSkills(cwd).values()) {
158
- if (skill.userInvocable && !result.has(skill.name)) {
159
- result.set(skill.name, { name: skill.name, description: skill.description });
160
- }
161
- }
162
- return Array.from(result.values());
163
- }
164
-
165
- export function expandSkillInvocation(text: string, cwd?: string): string {
166
- const trimmed = text.trim();
167
- if (!trimmed.startsWith("/")) return text;
168
- const match = trimmed.match(/^\/([a-zA-Z0-9_-]+)(?:\s+([\s\S]*))?$/);
169
- if (!match) return text;
170
-
171
- const [_, commandName, rawArgs] = match;
172
- const args = rawArgs ?? "";
173
- const skill = discoverSkills(cwd).get(commandName);
174
- if (!skill) return text;
175
-
176
- let expanded = skill.instructions;
177
- if (expanded.includes("$ARGUMENTS")) {
178
- expanded = expanded.replaceAll("$ARGUMENTS", args);
179
- } else if (args.trim().length > 0) {
180
- expanded = `${expanded}\n\n**Arguments:** ${args}`;
181
- }
182
- return `[Skill: ${skill.name}]\n${expanded}\n[/Skill: ${skill.name}]`;
183
- }