@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meshxdata/fops",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "CLI to install and manage Foundation data mesh platforms",
5
5
  "keywords": [
6
6
  "foundation",
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import https from "node:https";
4
5
  import chalk from "chalk";
5
6
  import { Command } from "commander";
6
7
  import { PKG } from "../config.js";
@@ -385,4 +386,118 @@ export function registerCommands(program, registry) {
385
386
  setPluginEnabled(id, false);
386
387
  console.log(chalk.yellow(` ○ Disabled plugin "${id}". Restart fops to apply.`));
387
388
  });
389
+
390
+ // ── Plugin marketplace ──────────────────────────────
391
+ const marketplaceCmd = pluginCmd
392
+ .command("marketplace")
393
+ .description("Browse and install plugins from GitHub");
394
+
395
+ marketplaceCmd
396
+ .command("add <repo>")
397
+ .description("Install a plugin from GitHub (owner/repo)")
398
+ .action(async (repo) => {
399
+ const parts = repo.split("/");
400
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
401
+ console.error(chalk.red(' Usage: fops plugin marketplace add <owner/repo>'));
402
+ process.exit(1);
403
+ }
404
+ const [owner, repoName] = parts;
405
+
406
+ // Download tarball from GitHub
407
+ const tarballUrl = `https://api.github.com/repos/${owner}/${repoName}/tarball/main`;
408
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "fops-marketplace-"));
409
+ const tarPath = path.join(tmpDir, "plugin.tar.gz");
410
+
411
+ console.log(chalk.blue(` Downloading ${owner}/${repoName}...`));
412
+
413
+ try {
414
+ await new Promise((resolve, reject) => {
415
+ const follow = (url) => {
416
+ https.get(url, { headers: { "User-Agent": "fops-cli", Accept: "application/vnd.github+json" } }, (res) => {
417
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
418
+ follow(res.headers.location);
419
+ return;
420
+ }
421
+ if (res.statusCode !== 200) {
422
+ reject(new Error(`GitHub returned ${res.statusCode}. Check that ${owner}/${repoName} exists and is public.`));
423
+ res.resume();
424
+ return;
425
+ }
426
+ const ws = fs.createWriteStream(tarPath);
427
+ res.pipe(ws);
428
+ ws.on("finish", resolve);
429
+ ws.on("error", reject);
430
+ }).on("error", reject);
431
+ };
432
+ follow(tarballUrl);
433
+ });
434
+ } catch (err) {
435
+ console.error(chalk.red(` Download failed: ${err.message}`));
436
+ fs.rmSync(tmpDir, { recursive: true, force: true });
437
+ process.exit(1);
438
+ }
439
+
440
+ // Extract tarball
441
+ const extractDir = path.join(tmpDir, "extracted");
442
+ fs.mkdirSync(extractDir, { recursive: true });
443
+ try {
444
+ await execa("tar", ["xzf", tarPath, "-C", extractDir]);
445
+ } catch (err) {
446
+ console.error(chalk.red(` Failed to extract archive: ${err.message}`));
447
+ fs.rmSync(tmpDir, { recursive: true, force: true });
448
+ process.exit(1);
449
+ }
450
+
451
+ // GitHub tarballs extract into a single directory like owner-repo-sha/
452
+ const extracted = fs.readdirSync(extractDir);
453
+ if (extracted.length === 0) {
454
+ console.error(chalk.red(" Archive was empty."));
455
+ fs.rmSync(tmpDir, { recursive: true, force: true });
456
+ process.exit(1);
457
+ }
458
+ const pluginSrc = path.join(extractDir, extracted[0]);
459
+
460
+ // Validate manifest
461
+ const manifestPath = path.join(pluginSrc, "fops.plugin.json");
462
+ if (!fs.existsSync(manifestPath)) {
463
+ console.error(chalk.red(` No fops.plugin.json found in ${owner}/${repoName}. Not a valid fops plugin.`));
464
+ fs.rmSync(tmpDir, { recursive: true, force: true });
465
+ process.exit(1);
466
+ }
467
+
468
+ let manifest;
469
+ try {
470
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
471
+ } catch {
472
+ console.error(chalk.red(" Invalid fops.plugin.json"));
473
+ fs.rmSync(tmpDir, { recursive: true, force: true });
474
+ process.exit(1);
475
+ }
476
+
477
+ if (!manifest.id) {
478
+ console.error(chalk.red(' fops.plugin.json missing required "id" field.'));
479
+ fs.rmSync(tmpDir, { recursive: true, force: true });
480
+ process.exit(1);
481
+ }
482
+
483
+ // Copy to ~/.fops/plugins/<id>/
484
+ const dest = path.join(os.homedir(), ".fops", "plugins", manifest.id);
485
+ fs.mkdirSync(dest, { recursive: true });
486
+
487
+ const entries = fs.readdirSync(pluginSrc, { withFileTypes: true });
488
+ for (const entry of entries) {
489
+ const srcFile = path.join(pluginSrc, entry.name);
490
+ const destFile = path.join(dest, entry.name);
491
+ if (entry.isFile()) {
492
+ fs.copyFileSync(srcFile, destFile);
493
+ } else if (entry.isDirectory()) {
494
+ fs.cpSync(srcFile, destFile, { recursive: true });
495
+ }
496
+ }
497
+
498
+ // Cleanup temp dir
499
+ fs.rmSync(tmpDir, { recursive: true, force: true });
500
+
501
+ console.log(chalk.green(` ✓ Installed plugin "${manifest.id}" from ${owner}/${repoName} to ${dest}`));
502
+ });
388
503
  }
package/src/doctor.js CHANGED
@@ -655,6 +655,10 @@ export async function runDoctor(opts = {}, registry = null) {
655
655
  const ERROR_RE = /\b(ERROR|FATAL|PANIC|CRITICAL)\b/;
656
656
  const CRASH_RE = /\b(OOM|OutOfMemory|out of memory|segmentation fault|segfault)\b/i;
657
657
  const CONN_RE = /\b(ECONNREFUSED|ETIMEDOUT|connection refused)\b/i;
658
+ // Telemetry/analytics hosts whose failures are harmless noise
659
+ const TELEMETRY_RE = /heapanalytics\.com|segment\.io|sentry\.io|amplitude\.com|mixpanel\.com|telemetry/i;
660
+ // Harmless startup noise — idempotent init scripts & transient Kafka topic races
661
+ const BENIGN_RE = /already exists|UNKNOWN_TOPIC_OR_PART/;
658
662
 
659
663
  for (const line of logOut.split("\n")) {
660
664
  const sep = line.indexOf(" | ");
@@ -662,6 +666,9 @@ export async function runDoctor(opts = {}, registry = null) {
662
666
  const service = line.slice(0, sep).trim();
663
667
  const msg = line.slice(sep + 3);
664
668
 
669
+ if (TELEMETRY_RE.test(msg)) continue;
670
+ if (BENIGN_RE.test(msg)) continue;
671
+
665
672
  let level = null;
666
673
  if (CRASH_RE.test(msg)) level = "crash";
667
674
  else if (ERROR_RE.test(msg)) level = "error";
@@ -0,0 +1,79 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import readline from "node:readline";
5
+ import { execSync } from "node:child_process";
6
+
7
+ const CREDENTIALS_PATH = path.join(os.homedir(), ".fops", "plugins", "coda", ".credentials.json");
8
+ const CODA_ACCOUNT_URL = "https://coda.io/account";
9
+
10
+ /**
11
+ * Prompt for a single line of input from the terminal.
12
+ */
13
+ function ask(question) {
14
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
15
+ return new Promise((resolve) => {
16
+ rl.question(question, (answer) => {
17
+ rl.close();
18
+ resolve(answer.trim());
19
+ });
20
+ });
21
+ }
22
+
23
+ /**
24
+ * Load saved credentials from disk.
25
+ */
26
+ export function loadCredentials() {
27
+ try {
28
+ if (!fs.existsSync(CREDENTIALS_PATH)) return null;
29
+ const raw = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, "utf8"));
30
+ if (!raw.access_token) return null;
31
+ return raw;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Save credentials to disk (mode 0o600).
39
+ */
40
+ function saveCredentials(creds) {
41
+ const dir = path.dirname(CREDENTIALS_PATH);
42
+ if (!fs.existsSync(dir)) {
43
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
44
+ }
45
+ fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
46
+ }
47
+
48
+ /**
49
+ * Open a URL in the user's default browser.
50
+ */
51
+ function openBrowser(url) {
52
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
53
+ try { execSync(`${cmd} '${url}'`, { stdio: "ignore" }); } catch {}
54
+ }
55
+
56
+ /**
57
+ * Run the Coda login flow — prompt user for an API token.
58
+ * Returns true on success, false on failure.
59
+ */
60
+ export async function runCodaLogin() {
61
+ console.log("\n Coda API Token Setup");
62
+ console.log(" ────────────────────");
63
+ console.log(` 1. Go to ${CODA_ACCOUNT_URL}`);
64
+ console.log(" 2. Scroll to \"API Settings\"");
65
+ console.log(" 3. Click \"Generate API token\" and copy it\n");
66
+
67
+ openBrowser(CODA_ACCOUNT_URL);
68
+
69
+ const token = await ask(" API token: ");
70
+ if (!token) {
71
+ console.log("\n Aborted.\n");
72
+ return false;
73
+ }
74
+
75
+ saveCredentials({ access_token: token });
76
+
77
+ console.log("\n Saved to ~/.fops/plugins/coda/.credentials.json\n");
78
+ return true;
79
+ }
@@ -0,0 +1,187 @@
1
+ import https from "node:https";
2
+ import { URL, URLSearchParams } from "node:url";
3
+
4
+ const BASE_URL = "https://coda.io/apis/v1";
5
+
6
+ /**
7
+ * Simple sliding-window rate limiter.
8
+ * Tracks timestamps of recent requests and delays when the window is full.
9
+ */
10
+ class RateLimiter {
11
+ constructor(maxRequests, windowMs) {
12
+ this.maxRequests = maxRequests;
13
+ this.windowMs = windowMs;
14
+ this.timestamps = [];
15
+ }
16
+
17
+ async wait() {
18
+ const now = Date.now();
19
+ this.timestamps = this.timestamps.filter((t) => now - t < this.windowMs);
20
+
21
+ if (this.timestamps.length >= this.maxRequests) {
22
+ const oldest = this.timestamps[0];
23
+ const delay = this.windowMs - (now - oldest) + 50; // 50ms buffer
24
+ await new Promise((r) => setTimeout(r, delay));
25
+ }
26
+
27
+ this.timestamps.push(Date.now());
28
+ }
29
+ }
30
+
31
+ // Conservative limits under Coda's hard caps (100 reads/6s, 4 doc listings/6s)
32
+ const readLimiter = new RateLimiter(90, 6000);
33
+ const docListLimiter = new RateLimiter(3, 6000);
34
+
35
+ /**
36
+ * Make an HTTPS request to the Coda API.
37
+ */
38
+ function request(method, path, token, body) {
39
+ return new Promise((resolve, reject) => {
40
+ const url = new URL(path, BASE_URL + "/");
41
+ const options = {
42
+ hostname: url.hostname,
43
+ port: 443,
44
+ path: url.pathname + url.search,
45
+ method,
46
+ headers: {
47
+ Authorization: `Bearer ${token}`,
48
+ "Content-Type": "application/json",
49
+ },
50
+ };
51
+
52
+ if (body) {
53
+ const encoded = JSON.stringify(body);
54
+ options.headers["Content-Length"] = Buffer.byteLength(encoded);
55
+ }
56
+
57
+ const req = https.request(options, (res) => {
58
+ let data = "";
59
+ res.on("data", (chunk) => { data += chunk; });
60
+ res.on("end", () => {
61
+ if (res.statusCode === 401 || res.statusCode === 403) {
62
+ reject(new Error(`Coda auth error (${res.statusCode}). Run: fops coda login`));
63
+ return;
64
+ }
65
+ if (res.statusCode === 429) {
66
+ reject(new Error("Coda rate limit exceeded. Wait a moment and retry."));
67
+ return;
68
+ }
69
+ if (res.statusCode >= 400) {
70
+ let msg = `Coda API error ${res.statusCode}`;
71
+ try { msg = JSON.parse(data).message || msg; } catch {}
72
+ reject(new Error(msg));
73
+ return;
74
+ }
75
+ try {
76
+ resolve(JSON.parse(data));
77
+ } catch {
78
+ reject(new Error(`Invalid JSON from Coda API: ${data.slice(0, 200)}`));
79
+ }
80
+ });
81
+ });
82
+
83
+ req.on("error", reject);
84
+ if (body) req.write(JSON.stringify(body));
85
+ req.end();
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Coda API client.
91
+ */
92
+ export class CodaClient {
93
+ constructor(token) {
94
+ this.token = token;
95
+ }
96
+
97
+ /** GET /whoami — verify token and get user info */
98
+ async whoami() {
99
+ await readLimiter.wait();
100
+ return request("GET", `${BASE_URL}/whoami`, this.token);
101
+ }
102
+
103
+ /** GET /docs — list or search docs */
104
+ async listDocs(query) {
105
+ await docListLimiter.wait();
106
+ let url = `${BASE_URL}/docs`;
107
+ if (query) url += `?query=${encodeURIComponent(query)}`;
108
+ return request("GET", url, this.token);
109
+ }
110
+
111
+ /** GET /docs/{docId}/pages — list pages in a doc */
112
+ async listPages(docId) {
113
+ await readLimiter.wait();
114
+ return request("GET", `${BASE_URL}/docs/${docId}/pages`, this.token);
115
+ }
116
+
117
+ /**
118
+ * Export a page as markdown (or html).
119
+ * POST /docs/{docId}/pages/{pageId}/export → poll until ready → return content.
120
+ */
121
+ async exportPage(docId, pageId, format = "markdown") {
122
+ await readLimiter.wait();
123
+ const exportReq = await request(
124
+ "POST",
125
+ `${BASE_URL}/docs/${docId}/pages/${pageId}/export`,
126
+ this.token,
127
+ { outputFormat: format },
128
+ );
129
+
130
+ const requestId = exportReq.id;
131
+ const deadline = Date.now() + 30_000;
132
+
133
+ while (Date.now() < deadline) {
134
+ await new Promise((r) => setTimeout(r, 1000));
135
+ await readLimiter.wait();
136
+
137
+ const status = await request(
138
+ "GET",
139
+ `${BASE_URL}/docs/${docId}/pages/${pageId}/export/${requestId}`,
140
+ this.token,
141
+ );
142
+
143
+ if (status.status === "complete") {
144
+ return status.downloadLink
145
+ ? await this._fetchDownload(status.downloadLink)
146
+ : status;
147
+ }
148
+
149
+ if (status.status === "failed") {
150
+ throw new Error(`Page export failed: ${status.error || "unknown"}`);
151
+ }
152
+ }
153
+
154
+ throw new Error("Page export timed out after 30s");
155
+ }
156
+
157
+ /** GET /docs/{docId}/tables — list tables */
158
+ async listTables(docId) {
159
+ await readLimiter.wait();
160
+ return request("GET", `${BASE_URL}/docs/${docId}/tables`, this.token);
161
+ }
162
+
163
+ /** GET /docs/{docId}/tables/{tableId}/rows — list/query rows */
164
+ async listRows(docId, tableId, query) {
165
+ await readLimiter.wait();
166
+ let url = `${BASE_URL}/docs/${docId}/tables/${tableId}/rows`;
167
+ if (query) url += `?query=${encodeURIComponent(query)}`;
168
+ return request("GET", url, this.token);
169
+ }
170
+
171
+ /** Fetch raw content from a download link (returned by export). */
172
+ _fetchDownload(url) {
173
+ return new Promise((resolve, reject) => {
174
+ const parsed = new URL(url);
175
+ https.get(parsed, (res) => {
176
+ // Follow redirects
177
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
178
+ this._fetchDownload(res.headers.location).then(resolve, reject);
179
+ return;
180
+ }
181
+ let data = "";
182
+ res.on("data", (chunk) => { data += chunk; });
183
+ res.on("end", () => resolve(data));
184
+ }).on("error", reject);
185
+ });
186
+ }
187
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "coda",
3
+ "name": "Coda Knowledge Base",
4
+ "version": "0.1.0",
5
+ "description": "Interactive Coda knowledge base for the fops agent",
6
+ "skills": ["skills/coda"]
7
+ }