@meshxdata/fops 0.0.4 → 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.
- package/package.json +2 -1
- package/src/commands/index.js +163 -1
- package/src/doctor.js +155 -17
- package/src/plugins/bundled/coda/auth.js +79 -0
- package/src/plugins/bundled/coda/client.js +187 -0
- package/src/plugins/bundled/coda/fops.plugin.json +7 -0
- package/src/plugins/bundled/coda/index.js +284 -0
- package/src/plugins/bundled/coda/package.json +3 -0
- package/src/plugins/bundled/coda/skills/coda/SKILL.md +82 -0
- package/src/plugins/bundled/cursor/fops.plugin.json +7 -0
- package/src/plugins/bundled/cursor/index.js +432 -0
- package/src/plugins/bundled/cursor/package.json +1 -0
- package/src/plugins/bundled/cursor/skills/cursor/SKILL.md +48 -0
- package/src/plugins/bundled/fops-plugin-1password/fops.plugin.json +7 -0
- package/src/plugins/bundled/fops-plugin-1password/index.js +239 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/env.js +100 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/op.js +111 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/setup.js +235 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/sync.js +61 -0
- package/src/plugins/bundled/fops-plugin-1password/package.json +1 -0
- package/src/plugins/bundled/fops-plugin-1password/skills/1password/SKILL.md +79 -0
- package/src/plugins/bundled/fops-plugin-ecr/fops.plugin.json +7 -0
- package/src/plugins/bundled/fops-plugin-ecr/index.js +302 -0
- package/src/plugins/bundled/fops-plugin-ecr/lib/aws.js +147 -0
- package/src/plugins/bundled/fops-plugin-ecr/lib/images.js +73 -0
- package/src/plugins/bundled/fops-plugin-ecr/lib/setup.js +180 -0
- package/src/plugins/bundled/fops-plugin-ecr/lib/sync.js +74 -0
- package/src/plugins/bundled/fops-plugin-ecr/package.json +1 -0
- package/src/plugins/bundled/fops-plugin-ecr/skills/ecr/SKILL.md +105 -0
- package/src/plugins/bundled/fops-plugin-memory/fops.plugin.json +7 -0
- package/src/plugins/bundled/fops-plugin-memory/index.js +148 -0
- package/src/plugins/bundled/fops-plugin-memory/lib/relevance.js +72 -0
- package/src/plugins/bundled/fops-plugin-memory/lib/store.js +75 -0
- package/src/plugins/bundled/fops-plugin-memory/package.json +1 -0
- package/src/plugins/bundled/fops-plugin-memory/skills/memory/SKILL.md +58 -0
- package/src/plugins/loader.js +40 -0
- package/src/setup/aws.js +51 -38
- package/src/setup/setup.js +2 -0
- package/src/setup/wizard.js +137 -12
|
@@ -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,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,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.
|