@meshxdata/fops 0.0.5 → 0.0.6

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 (38) hide show
  1. package/package.json +1 -1
  2. package/src/commands/index.js +115 -0
  3. package/src/doctor.js +7 -0
  4. package/src/plugins/bundled/coda/auth.js +79 -0
  5. package/src/plugins/bundled/coda/client.js +187 -0
  6. package/src/plugins/bundled/coda/fops.plugin.json +7 -0
  7. package/src/plugins/bundled/coda/index.js +284 -0
  8. package/src/plugins/bundled/coda/package.json +3 -0
  9. package/src/plugins/bundled/coda/skills/coda/SKILL.md +82 -0
  10. package/src/plugins/bundled/cursor/fops.plugin.json +7 -0
  11. package/src/plugins/bundled/cursor/index.js +432 -0
  12. package/src/plugins/bundled/cursor/package.json +1 -0
  13. package/src/plugins/bundled/cursor/skills/cursor/SKILL.md +48 -0
  14. package/src/plugins/bundled/fops-plugin-1password/fops.plugin.json +7 -0
  15. package/src/plugins/bundled/fops-plugin-1password/index.js +239 -0
  16. package/src/plugins/bundled/fops-plugin-1password/lib/env.js +100 -0
  17. package/src/plugins/bundled/fops-plugin-1password/lib/op.js +111 -0
  18. package/src/plugins/bundled/fops-plugin-1password/lib/setup.js +235 -0
  19. package/src/plugins/bundled/fops-plugin-1password/lib/sync.js +61 -0
  20. package/src/plugins/bundled/fops-plugin-1password/package.json +1 -0
  21. package/src/plugins/bundled/fops-plugin-1password/skills/1password/SKILL.md +79 -0
  22. package/src/plugins/bundled/fops-plugin-ecr/fops.plugin.json +7 -0
  23. package/src/plugins/bundled/fops-plugin-ecr/index.js +302 -0
  24. package/src/plugins/bundled/fops-plugin-ecr/lib/aws.js +147 -0
  25. package/src/plugins/bundled/fops-plugin-ecr/lib/images.js +73 -0
  26. package/src/plugins/bundled/fops-plugin-ecr/lib/setup.js +180 -0
  27. package/src/plugins/bundled/fops-plugin-ecr/lib/sync.js +74 -0
  28. package/src/plugins/bundled/fops-plugin-ecr/package.json +1 -0
  29. package/src/plugins/bundled/fops-plugin-ecr/skills/ecr/SKILL.md +105 -0
  30. package/src/plugins/bundled/fops-plugin-memory/fops.plugin.json +7 -0
  31. package/src/plugins/bundled/fops-plugin-memory/index.js +148 -0
  32. package/src/plugins/bundled/fops-plugin-memory/lib/relevance.js +72 -0
  33. package/src/plugins/bundled/fops-plugin-memory/lib/store.js +75 -0
  34. package/src/plugins/bundled/fops-plugin-memory/package.json +1 -0
  35. package/src/plugins/bundled/fops-plugin-memory/skills/memory/SKILL.md +58 -0
  36. package/src/plugins/loader.js +40 -0
  37. package/src/setup/setup.js +2 -0
  38. package/src/setup/wizard.js +12 -0
@@ -0,0 +1,432 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { execFileSync, spawn } from "node:child_process";
5
+
6
+ /**
7
+ * Resolve the cursor CLI binary path.
8
+ * Checks PATH first, then known macOS/Linux install locations.
9
+ */
10
+ function resolveCursorBin() {
11
+ // Check if cursor is on PATH
12
+ try {
13
+ execFileSync("cursor", ["--version"], { encoding: "utf8", timeout: 3000, stdio: "pipe" });
14
+ return "cursor";
15
+ } catch {
16
+ // Not on PATH — check common install locations
17
+ }
18
+
19
+ const candidates = [
20
+ // macOS: Cursor.app
21
+ "/Applications/Cursor.app/Contents/Resources/app/bin/cursor",
22
+ // macOS: user-local
23
+ path.join(os.homedir(), "Applications", "Cursor.app", "Contents", "Resources", "app", "bin", "cursor"),
24
+ // Linux: common install paths
25
+ "/usr/share/cursor/bin/cursor",
26
+ "/opt/cursor/bin/cursor",
27
+ path.join(os.homedir(), ".local", "bin", "cursor"),
28
+ ];
29
+
30
+ for (const candidate of candidates) {
31
+ if (fs.existsSync(candidate)) return candidate;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ let _cursorBin;
37
+ function cursorBin() {
38
+ if (_cursorBin === undefined) _cursorBin = resolveCursorBin();
39
+ return _cursorBin;
40
+ }
41
+
42
+ /**
43
+ * Run a command and return stdout, or null on failure.
44
+ */
45
+ function run(cmd, args, opts = {}) {
46
+ try {
47
+ return execFileSync(cmd, args, { encoding: "utf8", timeout: 5000, stdio: "pipe", ...opts }).trim();
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Spawn a command with inherited stdio (fire-and-forget for GUI apps).
55
+ */
56
+ function open(cmd, args) {
57
+ return new Promise((resolve, reject) => {
58
+ const child = spawn(cmd, args, { stdio: "inherit" });
59
+ child.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited with ${code}`))));
60
+ child.on("error", reject);
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Trigger Cursor's Composer (Cmd/Ctrl+I), paste the instruction, and submit.
66
+ * macOS: AppleScript, Linux: xdotool, Windows: PowerShell SendKeys.
67
+ */
68
+ function triggerComposer(instruction, files) {
69
+ const prompt = [
70
+ instruction,
71
+ "",
72
+ "Target files:",
73
+ ...files.map((f) => `- ${f}`),
74
+ ].join("\n");
75
+
76
+ if (process.platform === "darwin") {
77
+ const escaped = prompt.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
78
+ const script = `
79
+ delay 2
80
+ tell application "Cursor" to activate
81
+ delay 0.5
82
+ tell application "System Events"
83
+ tell process "Cursor"
84
+ keystroke "i" using {command down}
85
+ delay 0.8
86
+ set the clipboard to "${escaped}"
87
+ keystroke "v" using {command down}
88
+ delay 0.3
89
+ key code 36
90
+ end tell
91
+ end tell
92
+ `;
93
+ const child = spawn("osascript", ["-e", script], { stdio: "ignore", detached: true });
94
+ child.unref();
95
+ } else if (process.platform === "linux") {
96
+ // xdotool: wait for Cursor window, send Ctrl+I, type instruction, submit
97
+ const escaped = prompt.replace(/'/g, "'\\''");
98
+ const script = `
99
+ sleep 2
100
+ xdotool search --name "Cursor" windowactivate --sync
101
+ sleep 0.5
102
+ xdotool key ctrl+i
103
+ sleep 0.8
104
+ xdotool type --clearmodifiers -- '${escaped}'
105
+ sleep 0.3
106
+ xdotool key Return
107
+ `;
108
+ const child = spawn("bash", ["-c", script], { stdio: "ignore", detached: true });
109
+ child.unref();
110
+ } else if (process.platform === "win32") {
111
+ // PowerShell: activate Cursor, send Ctrl+I, paste, submit
112
+ const escaped = prompt.replace(/'/g, "''").replace(/`/g, "``");
113
+ const ps = `
114
+ Start-Sleep -Seconds 2
115
+ $wshell = New-Object -ComObject wscript.shell
116
+ $wshell.AppActivate('Cursor')
117
+ Start-Sleep -Milliseconds 500
118
+ $wshell.SendKeys('^i')
119
+ Start-Sleep -Milliseconds 800
120
+ Set-Clipboard '${escaped}'
121
+ $wshell.SendKeys('^v')
122
+ Start-Sleep -Milliseconds 300
123
+ $wshell.SendKeys('{ENTER}')
124
+ `;
125
+ const child = spawn("powershell", ["-NoProfile", "-Command", ps], { stdio: "ignore", detached: true });
126
+ child.unref();
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Parse SKILL.md frontmatter → { meta, body }.
132
+ */
133
+ function parseFrontmatter(content) {
134
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
135
+ if (!match) return { meta: {}, body: content };
136
+ const meta = {};
137
+ for (const line of match[1].split("\n")) {
138
+ const kv = line.match(/^(\w+)\s*:\s*(.+)/);
139
+ if (kv) meta[kv[1]] = kv[2].trim();
140
+ }
141
+ return { meta, body: match[2] };
142
+ }
143
+
144
+ /**
145
+ * Collect all SKILL.md files from known locations.
146
+ * Returns array of { name, description, content }.
147
+ */
148
+ function collectSkills() {
149
+ const skills = [];
150
+ const seen = new Set();
151
+
152
+ function scanDir(dir) {
153
+ if (!fs.existsSync(dir)) return;
154
+ try {
155
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
156
+ for (const entry of entries) {
157
+ if (!entry.isDirectory()) continue;
158
+ const skillMd = path.join(dir, entry.name, "SKILL.md");
159
+ if (!fs.existsSync(skillMd)) continue;
160
+ const raw = fs.readFileSync(skillMd, "utf8");
161
+ const { meta, body } = parseFrontmatter(raw);
162
+ const name = meta.name || entry.name;
163
+ if (seen.has(name)) continue;
164
+ seen.add(name);
165
+ skills.push({ name, description: meta.description || "", content: body.trim() });
166
+ }
167
+ } catch {
168
+ // ignore
169
+ }
170
+ }
171
+
172
+ // 1. Built-in CLI skills (resolve from the CLI entry point)
173
+ const cliSkillsDirs = [
174
+ process.argv[1] ? path.resolve(path.dirname(process.argv[1]), "src", "skills") : null,
175
+ path.resolve(os.homedir(), ".fops", "cli", "src", "skills"),
176
+ ].filter(Boolean);
177
+
178
+ for (const dir of cliSkillsDirs) {
179
+ scanDir(dir);
180
+ }
181
+
182
+ // 2. User skills ~/.fops/skills/
183
+ scanDir(path.join(os.homedir(), ".fops", "skills"));
184
+
185
+ // 3. Plugin skills ~/.fops/plugins/*/skills/
186
+ const pluginsDir = path.join(os.homedir(), ".fops", "plugins");
187
+ if (fs.existsSync(pluginsDir)) {
188
+ try {
189
+ const plugins = fs.readdirSync(pluginsDir, { withFileTypes: true });
190
+ for (const p of plugins) {
191
+ if (!p.isDirectory()) continue;
192
+ scanDir(path.join(pluginsDir, p.name, "skills"));
193
+ }
194
+ } catch {
195
+ // ignore
196
+ }
197
+ }
198
+
199
+ return skills;
200
+ }
201
+
202
+ /**
203
+ * Build .mdc frontmatter block.
204
+ */
205
+ function mdcFrontmatter({ description, globs, alwaysApply }) {
206
+ const lines = ["---"];
207
+ if (description) lines.push(`description: ${description}`);
208
+ if (globs) lines.push(`globs: ${globs}`);
209
+ if (alwaysApply != null) lines.push(`alwaysApply: ${alwaysApply}`);
210
+ lines.push("---");
211
+ return lines.join("\n");
212
+ }
213
+
214
+ /**
215
+ * Write .cursorrules + .cursor/rules/fops-*.mdc from collected skills.
216
+ * Returns number of skills synced.
217
+ */
218
+ function syncRules(cwd, skills) {
219
+ if (skills.length === 0) return 0;
220
+
221
+ // Write concatenated .cursorrules
222
+ const combined = skills
223
+ .map((s) => `# ${s.name}\n\n${s.content}`)
224
+ .join("\n\n---\n\n");
225
+ fs.writeFileSync(path.join(cwd, ".cursorrules"), combined, "utf8");
226
+
227
+ // Write individual .cursor/rules/fops-<name>.mdc files
228
+ const rulesDir = path.join(cwd, ".cursor", "rules");
229
+ fs.mkdirSync(rulesDir, { recursive: true });
230
+
231
+ for (const skill of skills) {
232
+ const safeName = skill.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
233
+ const front = mdcFrontmatter({
234
+ description: skill.description || skill.name,
235
+ alwaysApply: true,
236
+ });
237
+ fs.writeFileSync(path.join(rulesDir, `fops-${safeName}.mdc`), `${front}\n\n${skill.content}\n`, "utf8");
238
+ }
239
+
240
+ return skills.length;
241
+ }
242
+
243
+ export function register(api) {
244
+ // ── Doctor check ──────────────────────────────────
245
+ api.registerDoctorCheck({
246
+ name: "Cursor IDE",
247
+ fn: async (ok, warn) => {
248
+ const bin = cursorBin();
249
+ if (bin) {
250
+ const version = run(bin, ["--version"]);
251
+ ok("Cursor IDE", version || bin);
252
+ } else {
253
+ warn("Cursor IDE", "not found — install from cursor.com, then: Cmd+Shift+P → Shell Command: Install 'cursor'");
254
+ }
255
+ },
256
+ });
257
+
258
+ // ── Auto-run patterns (agent executes these immediately) ──
259
+ api.registerAutoRunPattern("fops cursor open");
260
+ api.registerAutoRunPattern("fops cursor edit");
261
+ api.registerAutoRunPattern("fops cursor rules sync");
262
+
263
+ // ── Commands ──────────────────────────────────────
264
+ api.registerCommand((program) => {
265
+ const cursor = program
266
+ .command("cursor")
267
+ .description("Cursor IDE integration — open files, sync rules, edit with AI");
268
+
269
+ // fops cursor open [path] [-g line]
270
+ cursor
271
+ .command("open [path]")
272
+ .description("Open file or folder in Cursor")
273
+ .option("-g <line>", "Go to line number")
274
+ .action(async (targetPath, opts) => {
275
+ const bin = cursorBin();
276
+ if (!bin) { console.error("Cursor CLI not found. Install from cursor.com, then: Cmd+Shift+P → 'Install cursor command'"); process.exit(1); }
277
+ const target = targetPath || ".";
278
+ const args = [];
279
+ if (opts.g) args.push("-g", `${target}:${opts.g}`);
280
+ else args.push(target);
281
+ try {
282
+ await open(bin, args);
283
+ } catch (err) {
284
+ console.error(`Failed to open Cursor: ${err.message}`);
285
+ process.exit(1);
286
+ }
287
+ });
288
+
289
+ // fops cursor rules sync | show | clean
290
+ const rules = cursor
291
+ .command("rules")
292
+ .description("Manage Cursor AI rules synced from fops skills");
293
+
294
+ rules
295
+ .command("sync")
296
+ .description("Sync all fops skills into .cursorrules and .cursor/rules/")
297
+ .action(async () => {
298
+ const cwd = process.cwd();
299
+ const skills = collectSkills();
300
+
301
+ if (skills.length === 0) {
302
+ console.log("No skills found to sync.");
303
+ return;
304
+ }
305
+
306
+ syncRules(cwd, skills);
307
+
308
+ console.log(`Synced ${skills.length} skill(s):`);
309
+ for (const s of skills) {
310
+ console.log(` - ${s.name}`);
311
+ }
312
+ console.log(`\nWrote .cursorrules and ${skills.length} .cursor/rules/fops-*.mdc file(s).`);
313
+ });
314
+
315
+ rules
316
+ .command("show")
317
+ .description("Display current .cursorrules and .cursor/rules/*.mdc files")
318
+ .action(async () => {
319
+ const cwd = process.cwd();
320
+ const cursorrules = path.join(cwd, ".cursorrules");
321
+
322
+ if (fs.existsSync(cursorrules)) {
323
+ console.log("── .cursorrules ──");
324
+ console.log(fs.readFileSync(cursorrules, "utf8"));
325
+ } else {
326
+ console.log("No .cursorrules file found. Run: fops cursor rules sync");
327
+ }
328
+
329
+ const rulesDir = path.join(cwd, ".cursor", "rules");
330
+ if (fs.existsSync(rulesDir)) {
331
+ try {
332
+ const files = fs.readdirSync(rulesDir).filter((f) => f.endsWith(".mdc"));
333
+ if (files.length > 0) {
334
+ console.log("\n── .cursor/rules/*.mdc ──");
335
+ for (const f of files) {
336
+ console.log(` ${f}`);
337
+ }
338
+ }
339
+ } catch {
340
+ // ignore
341
+ }
342
+ }
343
+ });
344
+
345
+ rules
346
+ .command("clean")
347
+ .description("Remove .cursorrules and fops-generated .cursor/rules/fops-*.mdc files")
348
+ .action(async () => {
349
+ const cwd = process.cwd();
350
+ let removed = 0;
351
+
352
+ const cursorrules = path.join(cwd, ".cursorrules");
353
+ if (fs.existsSync(cursorrules)) {
354
+ fs.unlinkSync(cursorrules);
355
+ console.log("Removed .cursorrules");
356
+ removed++;
357
+ }
358
+
359
+ const rulesDir = path.join(cwd, ".cursor", "rules");
360
+ if (fs.existsSync(rulesDir)) {
361
+ try {
362
+ const files = fs.readdirSync(rulesDir).filter((f) => f.startsWith("fops-") && f.endsWith(".mdc"));
363
+ for (const f of files) {
364
+ fs.unlinkSync(path.join(rulesDir, f));
365
+ console.log(`Removed .cursor/rules/${f}`);
366
+ removed++;
367
+ }
368
+ } catch {
369
+ // ignore
370
+ }
371
+ }
372
+
373
+ if (removed === 0) console.log("Nothing to clean.");
374
+ else console.log(`\nRemoved ${removed} file(s).`);
375
+ });
376
+
377
+ // fops cursor edit <files...> -m <instruction> [--rules-sync]
378
+ cursor
379
+ .command("edit <files...>")
380
+ .description("Open files in Cursor with a task-specific AI instruction")
381
+ .requiredOption("-m, --message <instruction>", "AI instruction for the task")
382
+ .option("--rules-sync", "Sync fops skills into .cursorrules before opening")
383
+ .action(async (files, opts) => {
384
+ const cwd = process.cwd();
385
+
386
+ // Auto-sync rules if flag is set or .cursorrules doesn't exist
387
+ if (opts.rulesSync || !fs.existsSync(path.join(cwd, ".cursorrules"))) {
388
+ const skills = collectSkills();
389
+ const count = syncRules(cwd, skills);
390
+ if (count > 0) {
391
+ console.log(`Synced ${count} skill(s) into .cursorrules`);
392
+ }
393
+ }
394
+
395
+ // Create task-specific rule
396
+ const rulesDir = path.join(cwd, ".cursor", "rules");
397
+ fs.mkdirSync(rulesDir, { recursive: true });
398
+
399
+ const globs = files.join(", ");
400
+ const front = mdcFrontmatter({
401
+ description: opts.message,
402
+ globs,
403
+ alwaysApply: true,
404
+ });
405
+ const body = [
406
+ "## Task",
407
+ "",
408
+ opts.message,
409
+ "",
410
+ "## Target files",
411
+ "",
412
+ ...files.map((f) => `- ${f}`),
413
+ "",
414
+ "Read the target files above and apply the instruction.",
415
+ ].join("\n");
416
+
417
+ fs.writeFileSync(path.join(rulesDir, "fops-task.mdc"), `${front}\n\n${body}\n`, "utf8");
418
+ console.log("Created .cursor/rules/fops-task.mdc");
419
+
420
+ // Open files in Cursor and trigger Composer with instruction
421
+ const bin = cursorBin();
422
+ if (!bin) { console.error("Cursor CLI not found. Install from cursor.com, then: Cmd+Shift+P → 'Install cursor command'"); process.exit(1); }
423
+ try {
424
+ await open(bin, files);
425
+ triggerComposer(opts.message, files);
426
+ } catch (err) {
427
+ console.error(`Failed to open Cursor: ${err.message}`);
428
+ process.exit(1);
429
+ }
430
+ });
431
+ });
432
+ }
@@ -0,0 +1 @@
1
+ { "type": "module" }
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: cursor
3
+ description: Cursor IDE integration — open files, sync AI rules, edit with instructions
4
+ requires: cursor
5
+ ---
6
+
7
+ ## Cursor IDE Commands
8
+
9
+ The `fops cursor` commands bridge fops knowledge into Cursor's AI via `.cursorrules` and `.cursor/rules/*.mdc` files.
10
+
11
+ **IMPORTANT: When the user asks to edit code, open a file, or work on something in Cursor — DO NOT just suggest the command. Output the `fops cursor edit` or `fops cursor open` command in a bash block and it will execute automatically.** Be direct: "Opening in Cursor..." not "you could run...".
12
+
13
+ ### When to act
14
+
15
+ - User says "edit X", "fix X", "refactor X", "open X in cursor" → immediately output `fops cursor edit <files> -m "<instruction>"`
16
+ - User says "open X" or "open in cursor" → immediately output `fops cursor open <path>`
17
+ - User asks to modify code with AI → `fops cursor edit`
18
+
19
+ ### Open files in Cursor
20
+
21
+ ```bash
22
+ fops cursor open # open current directory
23
+ fops cursor open src/api.py # open a specific file
24
+ fops cursor open src/api.py -g 42 # open at a specific line
25
+ ```
26
+
27
+ ### Sync fops skills as Cursor rules
28
+
29
+ ```bash
30
+ fops cursor rules sync # write all fops skills to .cursorrules + .cursor/rules/fops-*.mdc
31
+ fops cursor rules show # display current rules
32
+ fops cursor rules clean # remove fops-generated rules (keeps user-authored .mdc files)
33
+ ```
34
+
35
+ ### Edit with AI instructions
36
+
37
+ ```bash
38
+ fops cursor edit src/agent/context.js -m "add error handling for missing skills"
39
+ fops cursor edit src/api.py src/models.py -m "add pagination to the list endpoint"
40
+ ```
41
+
42
+ This creates a `.cursor/rules/fops-task.mdc` with the instruction and target files, then opens them in Cursor. If `.cursorrules` doesn't exist yet, it auto-syncs rules first.
43
+
44
+ Use `--rules-sync` to force a fresh rules sync before opening:
45
+
46
+ ```bash
47
+ fops cursor edit src/api.py -m "refactor auth" --rules-sync
48
+ ```
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "fops-plugin-1password",
3
+ "name": "1Password Secrets",
4
+ "version": "0.1.0",
5
+ "description": "Inject secrets from 1Password into .env files using op:// references",
6
+ "skills": ["skills/1password"]
7
+ }