@meshxdata/fops 0.0.5 → 0.0.7

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 (44) hide show
  1. package/package.json +3 -2
  2. package/src/auth/coda.js +4 -4
  3. package/src/auth/login.js +9 -6
  4. package/src/commands/index.js +158 -0
  5. package/src/doctor.js +340 -71
  6. package/src/feature-flags.js +3 -3
  7. package/src/lazy.js +12 -0
  8. package/src/plugins/bundled/coda/auth.js +85 -0
  9. package/src/plugins/bundled/coda/client.js +187 -0
  10. package/src/plugins/bundled/coda/fops.plugin.json +7 -0
  11. package/src/plugins/bundled/coda/index.js +284 -0
  12. package/src/plugins/bundled/coda/package.json +3 -0
  13. package/src/plugins/bundled/coda/skills/coda/SKILL.md +82 -0
  14. package/src/plugins/bundled/cursor/fops.plugin.json +7 -0
  15. package/src/plugins/bundled/cursor/index.js +433 -0
  16. package/src/plugins/bundled/cursor/package.json +1 -0
  17. package/src/plugins/bundled/cursor/skills/cursor/SKILL.md +48 -0
  18. package/src/plugins/bundled/fops-plugin-1password/fops.plugin.json +7 -0
  19. package/src/plugins/bundled/fops-plugin-1password/index.js +241 -0
  20. package/src/plugins/bundled/fops-plugin-1password/lib/env.js +100 -0
  21. package/src/plugins/bundled/fops-plugin-1password/lib/op.js +119 -0
  22. package/src/plugins/bundled/fops-plugin-1password/lib/setup.js +235 -0
  23. package/src/plugins/bundled/fops-plugin-1password/lib/sync.js +66 -0
  24. package/src/plugins/bundled/fops-plugin-1password/package.json +1 -0
  25. package/src/plugins/bundled/fops-plugin-1password/skills/1password/SKILL.md +79 -0
  26. package/src/plugins/bundled/fops-plugin-ecr/fops.plugin.json +7 -0
  27. package/src/plugins/bundled/fops-plugin-ecr/index.js +302 -0
  28. package/src/plugins/bundled/fops-plugin-ecr/lib/aws.js +146 -0
  29. package/src/plugins/bundled/fops-plugin-ecr/lib/images.js +73 -0
  30. package/src/plugins/bundled/fops-plugin-ecr/lib/setup.js +180 -0
  31. package/src/plugins/bundled/fops-plugin-ecr/lib/sync.js +74 -0
  32. package/src/plugins/bundled/fops-plugin-ecr/package.json +1 -0
  33. package/src/plugins/bundled/fops-plugin-ecr/skills/ecr/SKILL.md +105 -0
  34. package/src/plugins/bundled/fops-plugin-memory/fops.plugin.json +7 -0
  35. package/src/plugins/bundled/fops-plugin-memory/index.js +148 -0
  36. package/src/plugins/bundled/fops-plugin-memory/lib/relevance.js +72 -0
  37. package/src/plugins/bundled/fops-plugin-memory/lib/store.js +75 -0
  38. package/src/plugins/bundled/fops-plugin-memory/package.json +1 -0
  39. package/src/plugins/bundled/fops-plugin-memory/skills/memory/SKILL.md +58 -0
  40. package/src/plugins/loader.js +43 -3
  41. package/src/setup/aws.js +74 -46
  42. package/src/setup/setup.js +4 -2
  43. package/src/setup/wizard.js +16 -20
  44. package/src/wsl.js +82 -0
@@ -0,0 +1,85 @@
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
+ try {
53
+ if (process.platform === "win32") {
54
+ execSync(`cmd /c start "" "${url}"`, { stdio: "ignore" });
55
+ } else {
56
+ const cmd = process.platform === "darwin" ? "open" : "xdg-open";
57
+ execSync(`${cmd} '${url}'`, { stdio: "ignore" });
58
+ }
59
+ } catch {}
60
+ }
61
+
62
+ /**
63
+ * Run the Coda login flow — prompt user for an API token.
64
+ * Returns true on success, false on failure.
65
+ */
66
+ export async function runCodaLogin() {
67
+ console.log("\n Coda API Token Setup");
68
+ console.log(" ────────────────────");
69
+ console.log(` 1. Go to ${CODA_ACCOUNT_URL}`);
70
+ console.log(" 2. Scroll to \"API Settings\"");
71
+ console.log(" 3. Click \"Generate API token\" and copy it\n");
72
+
73
+ openBrowser(CODA_ACCOUNT_URL);
74
+
75
+ const token = await ask(" API token: ");
76
+ if (!token) {
77
+ console.log("\n Aborted.\n");
78
+ return false;
79
+ }
80
+
81
+ saveCredentials({ access_token: token });
82
+
83
+ console.log("\n Saved to ~/.fops/plugins/coda/.credentials.json\n");
84
+ return true;
85
+ }
@@ -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
+ }
@@ -0,0 +1,284 @@
1
+ import { CodaClient } from "./client.js";
2
+ import { loadCredentials, runCodaLogin } from "./auth.js";
3
+
4
+ function getClient() {
5
+ const creds = loadCredentials();
6
+ if (!creds) {
7
+ console.error("Not authenticated with Coda. Run: fops coda login");
8
+ process.exit(1);
9
+ }
10
+ return new CodaClient(creds.access_token);
11
+ }
12
+
13
+ export default {
14
+ async register(api) {
15
+ const config = api.config;
16
+
17
+ // ── Register "fops coda" command group ──────────────────────────────
18
+ api.registerCommand((program) => {
19
+ const coda = program
20
+ .command("coda")
21
+ .description("Coda knowledge base");
22
+
23
+ // fops coda login
24
+ coda
25
+ .command("login")
26
+ .description("Authenticate with Coda (API token)")
27
+ .action(async () => {
28
+ const ok = await runCodaLogin();
29
+ if (!ok) process.exit(1);
30
+ });
31
+
32
+ // fops coda whoami
33
+ coda
34
+ .command("whoami")
35
+ .description("Show current Coda user")
36
+ .action(async () => {
37
+ try {
38
+ const client = getClient();
39
+ const user = await client.whoami();
40
+ console.log(`Logged in as: ${user.name} (${user.loginId})`);
41
+ } catch (err) {
42
+ console.error(err.message);
43
+ process.exit(1);
44
+ }
45
+ });
46
+
47
+ // fops coda docs [-q <query>]
48
+ coda
49
+ .command("docs")
50
+ .description("List or search Coda docs")
51
+ .option("-q, --query <query>", "Filter docs by name")
52
+ .action(async (opts) => {
53
+ try {
54
+ const client = getClient();
55
+ const result = await client.listDocs(opts.query);
56
+ const docs = result.items || [];
57
+
58
+ if (docs.length === 0) {
59
+ console.log(opts.query ? `No docs matching "${opts.query}"` : "No docs found");
60
+ return;
61
+ }
62
+
63
+ for (const doc of docs) {
64
+ console.log(`${doc.id} ${doc.name}`);
65
+ }
66
+ console.log(`\n${docs.length} doc(s) found`);
67
+ } catch (err) {
68
+ console.error(err.message);
69
+ process.exit(1);
70
+ }
71
+ });
72
+
73
+ // fops coda search <query>
74
+ coda
75
+ .command("search <query>")
76
+ .description("Search docs and pages by name")
77
+ .action(async (query) => {
78
+ try {
79
+ const client = getClient();
80
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
81
+ const matches = (text) => words.every((w) => text.toLowerCase().includes(w));
82
+
83
+ // Fetch all docs (no query filter — API only searches doc names)
84
+ const result = await client.listDocs();
85
+ const docs = result.items || [];
86
+
87
+ let found = false;
88
+
89
+ for (const doc of docs) {
90
+ const docMatches = matches(doc.name);
91
+
92
+ let pages = [];
93
+ try {
94
+ const pageResult = await client.listPages(doc.id);
95
+ pages = pageResult.items || [];
96
+ } catch {
97
+ // skip if pages can't be listed
98
+ }
99
+
100
+ const matchingPages = pages.filter((p) => matches(p.name));
101
+
102
+ if (!docMatches && matchingPages.length === 0) continue;
103
+
104
+ found = true;
105
+ console.log(`\n── ${doc.name} (${doc.id}) ──`);
106
+
107
+ if (docMatches) {
108
+ // Doc name matches → show all pages
109
+ for (const page of pages) {
110
+ const marker = matches(page.name) ? " ◀" : "";
111
+ console.log(` ${page.id} ${page.name}${marker}`);
112
+ }
113
+ } else {
114
+ // Only page names match → show matching pages
115
+ for (const page of matchingPages) {
116
+ console.log(` ${page.id} ${page.name} ◀`);
117
+ }
118
+ }
119
+ }
120
+
121
+ if (!found) {
122
+ console.log(`No docs or pages matching "${query}"`);
123
+ }
124
+ } catch (err) {
125
+ console.error(err.message);
126
+ process.exit(1);
127
+ }
128
+ });
129
+
130
+ // fops coda read <docId> [pageId]
131
+ coda
132
+ .command("read <docId> [pageId]")
133
+ .description("List pages or read a page as markdown")
134
+ .action(async (docId, pageId) => {
135
+ try {
136
+ const client = getClient();
137
+
138
+ if (!pageId) {
139
+ // List pages
140
+ const result = await client.listPages(docId);
141
+ const pages = result.items || [];
142
+
143
+ if (pages.length === 0) {
144
+ console.log("No pages in this doc");
145
+ return;
146
+ }
147
+
148
+ for (const page of pages) {
149
+ console.log(`${page.id} ${page.name}`);
150
+ }
151
+ console.log(`\n${pages.length} page(s). Read one with: fops coda read ${docId} <pageId>`);
152
+ return;
153
+ }
154
+
155
+ // Export page as markdown
156
+ const content = await client.exportPage(docId, pageId);
157
+ console.log(typeof content === "string" ? content : JSON.stringify(content, null, 2));
158
+ } catch (err) {
159
+ console.error(err.message);
160
+ process.exit(1);
161
+ }
162
+ });
163
+
164
+ // fops coda tables <docId> [-t <tableId>] [-q <query>]
165
+ coda
166
+ .command("tables <docId>")
167
+ .description("List tables or query rows")
168
+ .option("-t, --table <tableId>", "Table ID to query rows from")
169
+ .option("-q, --query <query>", "Query to filter rows")
170
+ .action(async (docId, opts) => {
171
+ try {
172
+ const client = getClient();
173
+
174
+ if (!opts.table) {
175
+ // List tables
176
+ const result = await client.listTables(docId);
177
+ const tables = result.items || [];
178
+
179
+ if (tables.length === 0) {
180
+ console.log("No tables in this doc");
181
+ return;
182
+ }
183
+
184
+ for (const t of tables) {
185
+ console.log(`${t.id} ${t.name}`);
186
+ }
187
+ console.log(`\n${tables.length} table(s). Query rows with: fops coda tables ${docId} -t <tableId>`);
188
+ return;
189
+ }
190
+
191
+ // List/query rows
192
+ const result = await client.listRows(docId, opts.table, opts.query);
193
+ const rows = result.items || [];
194
+
195
+ if (rows.length === 0) {
196
+ console.log(opts.query ? `No rows matching "${opts.query}"` : "No rows found");
197
+ return;
198
+ }
199
+
200
+ for (const row of rows) {
201
+ const values = row.values || {};
202
+ const cells = Object.entries(values)
203
+ .map(([k, v]) => `${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`)
204
+ .join(" | ");
205
+ console.log(`${row.id} ${row.name || "(unnamed)"} ${cells}`);
206
+ }
207
+ console.log(`\n${rows.length} row(s)`);
208
+ } catch (err) {
209
+ console.error(err.message);
210
+ process.exit(1);
211
+ }
212
+ });
213
+ });
214
+
215
+ // ── Register doctor check ───────────────────────────────────────────
216
+ api.registerDoctorCheck({
217
+ name: "coda",
218
+ fn: async (ok, warn) => {
219
+ const creds = loadCredentials();
220
+ if (!creds) {
221
+ warn("Coda", "Not authenticated. Run: fops coda login");
222
+ return;
223
+ }
224
+
225
+ try {
226
+ const client = new CodaClient(creds.access_token);
227
+ const user = await client.whoami();
228
+ ok("Coda", `connected as ${user.name}`);
229
+ } catch (err) {
230
+ warn("Coda", `Token may be expired: ${err.message}. Run: fops coda login`);
231
+ }
232
+ },
233
+ });
234
+
235
+ // ── Register RAG knowledge source ─────────────────────────────────
236
+ api.registerKnowledgeSource({
237
+ name: "Coda",
238
+ description: "Search Coda docs and pages by name",
239
+ async search(query) {
240
+ const creds = loadCredentials();
241
+ if (!creds) return [];
242
+
243
+ try {
244
+ const client = new CodaClient(creds.access_token);
245
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
246
+ const matches = (text) => words.every((w) => text.toLowerCase().includes(w));
247
+
248
+ const result = await client.listDocs();
249
+ const docs = result.items || [];
250
+ const results = [];
251
+
252
+ for (const doc of docs) {
253
+ const docMatches = matches(doc.name);
254
+
255
+ let pages = [];
256
+ try {
257
+ const pageResult = await client.listPages(doc.id);
258
+ pages = pageResult.items || [];
259
+ } catch {
260
+ continue;
261
+ }
262
+
263
+ for (const page of pages) {
264
+ const pageMatches = matches(page.name);
265
+ if (!docMatches && !pageMatches) continue;
266
+
267
+ const score = docMatches && pageMatches ? 1.0 : docMatches ? 0.7 : 0.5;
268
+ results.push({
269
+ title: `${doc.name} → ${page.name}`,
270
+ content: `Coda page "${page.name}" in doc "${doc.name}".\nRead with: fops coda read ${doc.id} ${page.id}`,
271
+ url: `https://coda.io/d/${doc.id}/_/${page.id}`,
272
+ score,
273
+ });
274
+ }
275
+ }
276
+
277
+ return results.sort((a, b) => b.score - a.score).slice(0, 10);
278
+ } catch {
279
+ return [];
280
+ }
281
+ },
282
+ });
283
+ },
284
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: coda
3
+ description: Search and read Coda knowledge base docs interactively
4
+ ---
5
+ ## Coda Knowledge Base
6
+
7
+ Use `fops coda` commands to search and read internal documentation, runbooks, architecture decisions, and process docs stored in Coda.
8
+
9
+ ### When to Use
10
+
11
+ - User asks about internal processes, onboarding, or "how we do X"
12
+ - User needs a runbook, playbook, or architecture doc
13
+ - User references a doc that might be in Coda
14
+ - User asks "where is the doc for..." or "find the page about..."
15
+
16
+ ### Discovery Pattern
17
+
18
+ Always follow: **search → list pages → read page**.
19
+
20
+ 1. Search for relevant docs by name:
21
+ ```bash
22
+ fops coda docs -q "deployment"
23
+ ```
24
+
25
+ 2. Explore a doc's page structure:
26
+ ```bash
27
+ fops coda read <docId>
28
+ ```
29
+
30
+ 3. Read a specific page as markdown:
31
+ ```bash
32
+ fops coda read <docId> <pageId>
33
+ ```
34
+
35
+ ### Commands
36
+
37
+ **List or search docs:**
38
+ ```bash
39
+ fops coda docs
40
+ fops coda docs -q "onboarding"
41
+ ```
42
+
43
+ **Full-text search across docs (shows page structure):**
44
+ ```bash
45
+ fops coda search "incident response"
46
+ ```
47
+
48
+ **Read pages from a doc:**
49
+ ```bash
50
+ fops coda read <docId> # list all pages
51
+ fops coda read <docId> <pageId> # export page as markdown
52
+ ```
53
+
54
+ **List tables or query rows:**
55
+ ```bash
56
+ fops coda tables <docId> # list tables
57
+ fops coda tables <docId> -t <tableId> # list rows
58
+ fops coda tables <docId> -t <tableId> -q "status:active" # query rows
59
+ ```
60
+
61
+ **Auth:**
62
+ ```bash
63
+ fops coda login # authenticate with Coda
64
+ fops coda whoami # check current auth
65
+ ```
66
+
67
+ ### Suggesting Commands
68
+
69
+ Always suggest **2 commands** so the user can discover and then read:
70
+
71
+ - Search + read: `fops coda docs -q "runbook"` + `fops coda read <docId>`
72
+ - List + query: `fops coda tables <docId>` + `fops coda tables <docId> -t <tableId>`
73
+ - Broad + narrow: `fops coda search "deploy"` + `fops coda read <docId> <pageId>`
74
+
75
+ ### Error Handling
76
+
77
+ If a command returns an auth error, suggest:
78
+ ```bash
79
+ fops coda login
80
+ ```
81
+
82
+ Then retry the original command.
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "cursor",
3
+ "name": "Cursor IDE Integration",
4
+ "version": "0.1.0",
5
+ "description": "Bridge fops knowledge into Cursor AI via rules and edit commands",
6
+ "skills": ["skills/cursor"]
7
+ }