@viraatdas/rudder 3.8.2 → 3.9.0
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/README.md +12 -0
- package/assets/skills/google-cloud/SKILL.md +121 -0
- package/assets/skills/google-cloud/scripts/gcp.mjs +390 -0
- package/dist/agent-skills.d.ts +16 -0
- package/dist/agent-skills.js +68 -0
- package/dist/agent-skills.js.map +1 -0
- package/dist/attach-session.d.ts +1 -0
- package/dist/attach-session.js +8 -1
- package/dist/attach-session.js.map +1 -1
- package/dist/cloud.js +15 -0
- package/dist/cloud.js.map +1 -1
- package/dist/main.js +3 -0
- package/dist/main.js.map +1 -1
- package/dist/native/darwin-arm64/rudder-native +0 -0
- package/dist/native/darwin-x64/rudder-native +0 -0
- package/dist/native/linux-x64/rudder-native +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -286,6 +286,18 @@ the highlighted one's result below it, and `Esc` backs out. `review` stays inlin
|
|
|
286
286
|
up to five rows — merge-ready work should be one keystroke away — and collapses the
|
|
287
287
|
same way past that, because a 25-row review section is not scannable either.
|
|
288
288
|
|
|
289
|
+
## Agent skills
|
|
290
|
+
|
|
291
|
+
Rudder installs capability skills for every Claude Code and Codex agent it runs
|
|
292
|
+
(user-level, as `rudder-<name>`):
|
|
293
|
+
|
|
294
|
+
- **rudder-google-cloud**: create and name Google Cloud projects, enable APIs,
|
|
295
|
+
and set up "Sign in with Google" so the consent screen shows your product's
|
|
296
|
+
name, with a checker that verifies redirect URIs and the app name the way a
|
|
297
|
+
signed-out user sees them.
|
|
298
|
+
|
|
299
|
+
Set `RUDDER_DISABLE_AGENT_SKILLS=1` to skip.
|
|
300
|
+
|
|
289
301
|
## Commands and Orchestrator Skills
|
|
290
302
|
|
|
291
303
|
Type these in the bottom task input. When an interactive Claude orchestrator is
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rudder-google-cloud
|
|
3
|
+
description: Use for any Google Cloud or Google API setup — creating or naming a GCP project, enabling APIs (Gmail, Calendar, Drive, Gemini, …), "Sign in with Google" / Google OAuth clients, the consent screen (app name, logo, domains, scopes, verification, test users), redirect_uri_mismatch and other OAuth errors, gcloud auth, service accounts and keys, and storing the resulting secrets. Makes sure users see the product's own name on Google's screens, never a stray project like "My First Project".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Google Cloud, done properly
|
|
7
|
+
|
|
8
|
+
The failure this skill exists to prevent: a user clicks "Sign in with Google" and
|
|
9
|
+
sees **"to continue to e2mail-494301"** or **"My First Project"** (or an
|
|
10
|
+
unverified-app wall) because the OAuth client was dropped into whatever project
|
|
11
|
+
was lying around. Every step below ends with a check of what a real user sees.
|
|
12
|
+
|
|
13
|
+
Helper: `node <this skill dir>/scripts/gcp.mjs <command>` (Node 18+, no deps).
|
|
14
|
+
It finds credentials from `gcloud`, gcloud's stored login, or ADC; run
|
|
15
|
+
`whoami` first.
|
|
16
|
+
|
|
17
|
+
## The rules
|
|
18
|
+
|
|
19
|
+
1. **One project per product, named after the product.** Display name = the
|
|
20
|
+
product name exactly as marketed ("Tempo"); project ID = `<product>-<org>`
|
|
21
|
+
(immutable, globally unique, 6–30 chars). Never reuse another product's
|
|
22
|
+
project, a "My First Project", or a `gen-lang-client-*` Gemini project.
|
|
23
|
+
Create it under the user's organization (the helper picks the only org
|
|
24
|
+
automatically).
|
|
25
|
+
2. **The consent screen's App name is what users read**, not the project name
|
|
26
|
+
and not the client name. Set it on the Branding page to the product name,
|
|
27
|
+
with the product's own support email, home page, privacy and terms URLs, and
|
|
28
|
+
the product's domain as an authorized domain.
|
|
29
|
+
3. **Ask for the narrowest scopes that work.** Basic (openid/email/profile) can go
|
|
30
|
+
straight to production. Sensitive (Calendar, gmail.send) need verification.
|
|
31
|
+
Restricted (Gmail read/modify/compose, Drive full) need verification **and**
|
|
32
|
+
a CASA security assessment before more than 100 users; until then keep the
|
|
33
|
+
app in Testing and add test users. Tell the user this up front.
|
|
34
|
+
4. **Secrets never touch chat or git.** Read the client secret once, write it
|
|
35
|
+
straight into the host's secret store (`fly secrets set`, `vercel env add`,
|
|
36
|
+
GitHub secrets), and refer to it by name afterwards.
|
|
37
|
+
5. **Done means verified from outside**: `check-client` passes for every
|
|
38
|
+
redirect URI with the expected app name, and one real sign-in in a browser
|
|
39
|
+
reaches the app.
|
|
40
|
+
|
|
41
|
+
## Workflow
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
G="node <skill dir>/scripts/gcp.mjs"
|
|
45
|
+
$G whoami # account, scopes, org
|
|
46
|
+
$G projects --filter <product> # is there already a right project?
|
|
47
|
+
$G ensure-project --id <product>-<org> --name "<Product>" # create, or fix the display name
|
|
48
|
+
$G enable --project <id> --apis gmail,calendar # aliases: drive sheets docs people gemini vertex run storage …
|
|
49
|
+
$G audit --project <id> # name, parent, enabled APIs
|
|
50
|
+
$G oauth-plan --project <id> --name "<Product>" --home https://<domain> \
|
|
51
|
+
--support <owner email> --redirect https://<domain>/<callback> [--redirect …] \
|
|
52
|
+
[--scopes gmail.readonly,calendar.readonly]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`oauth-plan` prints the exact Console pages and the value for every field.
|
|
56
|
+
**Branding, Audience, Data access and Clients have no public API** (the old
|
|
57
|
+
`gcloud alpha iap oauth-*` commands were shut down in March 2026; `gcloud iam
|
|
58
|
+
oauth-clients` is for workforce identity, not "Sign in with Google"). Do them:
|
|
59
|
+
|
|
60
|
+
- **With browser tools** (Claude in Chrome or the built-in browser): open each
|
|
61
|
+
URL from the plan, confirm the signed-in account chip is an owner of the
|
|
62
|
+
project, fill the fields verbatim, save. On the client page, copy the ID and
|
|
63
|
+
secret before closing the dialog — the secret is not shown again. Never type
|
|
64
|
+
the user's Google password; if the browser is signed out, stop and ask the
|
|
65
|
+
user to sign in.
|
|
66
|
+
- **Without a browser**: hand the user the plan output as a checklist and ask
|
|
67
|
+
them to paste back the client ID and secret, or to set the secrets on the
|
|
68
|
+
host themselves (`! fly secrets set …`).
|
|
69
|
+
|
|
70
|
+
Then store and verify:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
fly secrets set -a <app> GOOGLE_CLIENT_ID=<id> GOOGLE_CLIENT_SECRET=<secret> # or vercel env add / gh secret set
|
|
74
|
+
$G check-client --client-id <id> --expect-name "<Product>" \
|
|
75
|
+
--redirect https://<domain>/<callback> [--redirect …]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`check-client` asks Google's real authorization endpoint, signed out, exactly
|
|
79
|
+
as a new user's browser would. It catches `redirect_uri_mismatch`,
|
|
80
|
+
`invalid_client`, `deleted_client`, Internal-only audiences and a wrong app
|
|
81
|
+
name ("continue to e2mail-494301") without anyone signing in. Finish with one
|
|
82
|
+
real sign-in in a browser and say in your report what the screen showed.
|
|
83
|
+
|
|
84
|
+
## Getting the details right
|
|
85
|
+
|
|
86
|
+
- **Redirect URIs match byte for byte**: scheme, host, port, path and trailing
|
|
87
|
+
slash. `https` only, except `http://localhost`/`127.0.0.1` for local dev. Add
|
|
88
|
+
the production URI and the local one if the app is developed locally; keep
|
|
89
|
+
staging on its own client if its domain differs.
|
|
90
|
+
- **Authorized JavaScript origins** are only for the Google Identity Services
|
|
91
|
+
button/One Tap (browser-side flows); server-side redirect flows don't need them.
|
|
92
|
+
- **Native apps** (iOS/Android/desktop) get their own client of that type; iOS
|
|
93
|
+
uses the reversed client ID as a URL scheme. Don't reuse the web client.
|
|
94
|
+
- **Testing vs production**: in Testing only listed test users can sign in and
|
|
95
|
+
refresh tokens expire after 7 days — fine for a demo, wrong for real users.
|
|
96
|
+
Publish as soon as the scopes allow it.
|
|
97
|
+
- **Logo**: uploading one triggers brand verification; leave it off until the
|
|
98
|
+
user is ready to verify. The name, links and domain still show.
|
|
99
|
+
- **Internal user type** restricts sign-in to the org's Workspace accounts — only
|
|
100
|
+
right for internal tools.
|
|
101
|
+
- **Verification** (sensitive/restricted scopes): needs the home page on the
|
|
102
|
+
authorized domain, a privacy policy describing Google data use (Limited Use),
|
|
103
|
+
a demo video of the consent flow, and for restricted scopes a CASA
|
|
104
|
+
assessment. Domain ownership is verified in Search Console by the same
|
|
105
|
+
account. Note it as a follow-up with the 100-user cap; don't silently ship.
|
|
106
|
+
- **Service accounts** for server-to-server: prefer workload identity / attached
|
|
107
|
+
service accounts over JSON keys; if a key is unavoidable, create it with
|
|
108
|
+
`gcloud iam service-accounts keys create` straight into the secret store and
|
|
109
|
+
delete the local file.
|
|
110
|
+
- **Billing** isn't needed for OAuth, Gmail or Calendar; it is for Maps, Vertex,
|
|
111
|
+
Cloud Run and most paid APIs. Check before promising those.
|
|
112
|
+
- **Renaming later** is safe for the project display name (`ensure-project`
|
|
113
|
+
again) and the consent App name (Branding page). The project ID can never
|
|
114
|
+
change — choose it once, deliberately.
|
|
115
|
+
|
|
116
|
+
## Report
|
|
117
|
+
|
|
118
|
+
Say which project (ID and display name), which APIs, which client (ID, never
|
|
119
|
+
the secret), where the secrets live, the `check-client` result per redirect
|
|
120
|
+
URI, what the real sign-in screen showed, the publishing status, and any
|
|
121
|
+
verification still owed (with the user cap it implies).
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Google Cloud helper for agents. No dependencies; Node 18+.
|
|
3
|
+
//
|
|
4
|
+
// Does everything Google exposes an API for (identity, projects, APIs) and,
|
|
5
|
+
// for the parts that only exist in the Cloud Console (OAuth branding and
|
|
6
|
+
// clients), prints the exact pages and values, then verifies the result from
|
|
7
|
+
// the outside the way a user would see it.
|
|
8
|
+
//
|
|
9
|
+
// node gcp.mjs whoami
|
|
10
|
+
// node gcp.mjs projects [--filter text]
|
|
11
|
+
// node gcp.mjs ensure-project --id tempo-exla --name "Tempo" [--org 123 | --folder 456]
|
|
12
|
+
// node gcp.mjs enable --project tempo-exla --apis gmail,calendar
|
|
13
|
+
// node gcp.mjs audit --project tempo-exla
|
|
14
|
+
// node gcp.mjs oauth-plan --project tempo-exla --name "Tempo" --home https://tempo.example.com \
|
|
15
|
+
// --support you@example.com --redirect https://tempo.example.com/api/auth/callback/google [--redirect …] \
|
|
16
|
+
// [--scopes gmail.readonly,calendar.readonly] [--privacy URL] [--terms URL] [--domain example.com]
|
|
17
|
+
// node gcp.mjs check-client --client-id ID --redirect URI [--redirect …] [--expect-name "Tempo"] [--scopes …]
|
|
18
|
+
//
|
|
19
|
+
// Auth, in order: GOOGLE_OAUTH_ACCESS_TOKEN, `gcloud auth print-access-token`,
|
|
20
|
+
// gcloud's stored user credentials, application-default credentials.
|
|
21
|
+
|
|
22
|
+
import { execFileSync } from "node:child_process";
|
|
23
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
|
|
27
|
+
const API_ALIASES = {
|
|
28
|
+
gmail: "gmail.googleapis.com",
|
|
29
|
+
calendar: "calendar-json.googleapis.com",
|
|
30
|
+
drive: "drive.googleapis.com",
|
|
31
|
+
sheets: "sheets.googleapis.com",
|
|
32
|
+
docs: "docs.googleapis.com",
|
|
33
|
+
people: "people.googleapis.com",
|
|
34
|
+
contacts: "people.googleapis.com",
|
|
35
|
+
youtube: "youtube.googleapis.com",
|
|
36
|
+
maps: "maps-backend.googleapis.com",
|
|
37
|
+
places: "places.googleapis.com",
|
|
38
|
+
gemini: "generativelanguage.googleapis.com",
|
|
39
|
+
vertex: "aiplatform.googleapis.com",
|
|
40
|
+
firebase: "firebase.googleapis.com",
|
|
41
|
+
run: "run.googleapis.com",
|
|
42
|
+
storage: "storage.googleapis.com",
|
|
43
|
+
secretmanager: "secretmanager.googleapis.com",
|
|
44
|
+
pubsub: "pubsub.googleapis.com",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Scope shorthands → full scope, and Google's sensitivity class (drives verification).
|
|
48
|
+
const SCOPES = {
|
|
49
|
+
"openid": ["openid", "basic"],
|
|
50
|
+
"email": ["https://www.googleapis.com/auth/userinfo.email", "basic"],
|
|
51
|
+
"profile": ["https://www.googleapis.com/auth/userinfo.profile", "basic"],
|
|
52
|
+
"gmail.readonly": ["https://www.googleapis.com/auth/gmail.readonly", "restricted"],
|
|
53
|
+
"gmail.modify": ["https://www.googleapis.com/auth/gmail.modify", "restricted"],
|
|
54
|
+
"gmail.compose": ["https://www.googleapis.com/auth/gmail.compose", "restricted"],
|
|
55
|
+
"gmail.send": ["https://www.googleapis.com/auth/gmail.send", "sensitive"],
|
|
56
|
+
"gmail.metadata": ["https://www.googleapis.com/auth/gmail.metadata", "restricted"],
|
|
57
|
+
"calendar.readonly": ["https://www.googleapis.com/auth/calendar.readonly", "sensitive"],
|
|
58
|
+
"calendar.events": ["https://www.googleapis.com/auth/calendar.events", "sensitive"],
|
|
59
|
+
"calendar": ["https://www.googleapis.com/auth/calendar", "sensitive"],
|
|
60
|
+
"drive.file": ["https://www.googleapis.com/auth/drive.file", "basic"],
|
|
61
|
+
"drive.readonly": ["https://www.googleapis.com/auth/drive.readonly", "restricted"],
|
|
62
|
+
"drive": ["https://www.googleapis.com/auth/drive", "restricted"],
|
|
63
|
+
"contacts.readonly": ["https://www.googleapis.com/auth/contacts.readonly", "sensitive"],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function parseArgs(argv) {
|
|
67
|
+
const [command, ...rest] = argv;
|
|
68
|
+
const flags = {};
|
|
69
|
+
for (let i = 0; i < rest.length; i++) {
|
|
70
|
+
const a = rest[i];
|
|
71
|
+
if (!a.startsWith("--")) continue;
|
|
72
|
+
const key = a.slice(2);
|
|
73
|
+
const value = rest[i + 1] && !rest[i + 1].startsWith("--") ? rest[++i] : "true";
|
|
74
|
+
(flags[key] ??= []).push(value);
|
|
75
|
+
}
|
|
76
|
+
const one = (k) => flags[k]?.[flags[k].length - 1];
|
|
77
|
+
const many = (k) => (flags[k] ?? []).flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
|
|
78
|
+
return { command, one, many };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fail(message) {
|
|
82
|
+
console.error(`✗ ${message}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Auth
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
function gcloudConfigDir() {
|
|
91
|
+
return process.env.CLOUDSDK_CONFIG || join(homedir(), ".config", "gcloud");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function activeAccount() {
|
|
95
|
+
try {
|
|
96
|
+
return execFileSync("gcloud", ["config", "get-value", "account"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
97
|
+
} catch {}
|
|
98
|
+
const dir = gcloudConfigDir();
|
|
99
|
+
try {
|
|
100
|
+
const active = readFileSync(join(dir, "active_config"), "utf8").trim() || "default";
|
|
101
|
+
const config = readFileSync(join(dir, "configurations", `config_${active}`), "utf8");
|
|
102
|
+
return config.match(/^\s*account\s*=\s*(.+)$/m)?.[1]?.trim() ?? null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function refresh(cred) {
|
|
109
|
+
const response = await fetch("https://oauth2.googleapis.com/token", {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
112
|
+
body: new URLSearchParams({ client_id: cred.client_id, client_secret: cred.client_secret, refresh_token: cred.refresh_token, grant_type: "refresh_token" }),
|
|
113
|
+
});
|
|
114
|
+
const data = await response.json();
|
|
115
|
+
if (!response.ok) throw new Error(`token refresh failed: ${data.error_description || data.error}`);
|
|
116
|
+
return data.access_token;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let cachedToken;
|
|
120
|
+
async function token() {
|
|
121
|
+
if (cachedToken) return cachedToken;
|
|
122
|
+
if (process.env.GOOGLE_OAUTH_ACCESS_TOKEN) return (cachedToken = process.env.GOOGLE_OAUTH_ACCESS_TOKEN);
|
|
123
|
+
try {
|
|
124
|
+
const t = execFileSync("gcloud", ["auth", "print-access-token"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
125
|
+
if (t) return (cachedToken = t);
|
|
126
|
+
} catch {}
|
|
127
|
+
const dir = gcloudConfigDir();
|
|
128
|
+
const account = activeAccount();
|
|
129
|
+
const candidates = [];
|
|
130
|
+
if (account) candidates.push(join(dir, "legacy_credentials", account, "adc.json"));
|
|
131
|
+
if (existsSync(join(dir, "legacy_credentials"))) {
|
|
132
|
+
for (const a of readdirSync(join(dir, "legacy_credentials"))) candidates.push(join(dir, "legacy_credentials", a, "adc.json"));
|
|
133
|
+
}
|
|
134
|
+
candidates.push(process.env.GOOGLE_APPLICATION_CREDENTIALS ?? "", join(dir, "application_default_credentials.json"));
|
|
135
|
+
for (const file of candidates) {
|
|
136
|
+
if (!file || !existsSync(file)) continue;
|
|
137
|
+
const cred = JSON.parse(readFileSync(file, "utf8"));
|
|
138
|
+
if (cred.type === "authorized_user") return (cachedToken = await refresh(cred));
|
|
139
|
+
}
|
|
140
|
+
fail("No Google credentials. Ask the user to run `! gcloud auth login` (and `! gcloud auth application-default login` for ADC), or install the SDK: https://cloud.google.com/sdk/docs/install");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function api(method, url, body) {
|
|
144
|
+
const response = await fetch(url, {
|
|
145
|
+
method,
|
|
146
|
+
headers: { Authorization: `Bearer ${await token()}`, "content-type": "application/json" },
|
|
147
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
148
|
+
});
|
|
149
|
+
const text = await response.text();
|
|
150
|
+
const data = text ? JSON.parse(text) : {};
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
const err = new Error(`${method} ${url.replace(/^https:\/\//, "")}: ${response.status} ${data.error?.message ?? text.slice(0, 300)}`);
|
|
153
|
+
err.status = response.status;
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
return data;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function wait(operation, base) {
|
|
160
|
+
let op = operation;
|
|
161
|
+
for (let i = 0; i < 60 && !op.done; i++) {
|
|
162
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
163
|
+
op = await api("GET", `${base}/${op.name}`);
|
|
164
|
+
}
|
|
165
|
+
if (op.error) throw new Error(`operation failed: ${op.error.message}`);
|
|
166
|
+
return op;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Commands
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
async function whoami() {
|
|
174
|
+
const info = await api("GET", "https://www.googleapis.com/oauth2/v3/userinfo").catch(() => null);
|
|
175
|
+
const tokenInfo = await (await fetch(`https://oauth2.googleapis.com/tokeninfo?access_token=${await token()}`)).json();
|
|
176
|
+
console.log(`account: ${info?.email ?? tokenInfo.email ?? activeAccount() ?? "unknown"}`);
|
|
177
|
+
console.log(`scopes: ${(tokenInfo.scope ?? "").split(" ").map((s) => s.replace("https://www.googleapis.com/auth/", "")).join(", ")}`);
|
|
178
|
+
const orgs = await api("POST", "https://cloudresourcemanager.googleapis.com/v1/organizations:search", {}).catch(() => ({}));
|
|
179
|
+
for (const o of orgs.organizations ?? []) console.log(`org: ${o.displayName} (${o.name.split("/")[1]})`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function listProjects(filter) {
|
|
183
|
+
const out = [];
|
|
184
|
+
let pageToken;
|
|
185
|
+
do {
|
|
186
|
+
const page = await api("GET", `https://cloudresourcemanager.googleapis.com/v1/projects?pageSize=500${pageToken ? `&pageToken=${pageToken}` : ""}`);
|
|
187
|
+
out.push(...(page.projects ?? []));
|
|
188
|
+
pageToken = page.nextPageToken;
|
|
189
|
+
} while (pageToken);
|
|
190
|
+
return out.filter((p) => p.lifecycleState === "ACTIVE" && (!filter || `${p.projectId} ${p.name}`.toLowerCase().includes(filter.toLowerCase())));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function projects(filter) {
|
|
194
|
+
const list = await listProjects(filter);
|
|
195
|
+
for (const p of list) console.log(`${p.projectId.padEnd(34)} ${JSON.stringify(p.name).padEnd(30)} ${p.parent ? `${p.parent.type}/${p.parent.id}` : "no parent"}`);
|
|
196
|
+
console.log(`${list.length} project(s)`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const PROJECT_ID = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
|
|
200
|
+
|
|
201
|
+
async function ensureProject(id, name, org, folder) {
|
|
202
|
+
if (!id || !name) fail("--id and --name are required (name is what people see; use the product's name)");
|
|
203
|
+
if (!PROJECT_ID.test(id)) fail(`"${id}" is not a valid project ID: 6-30 chars, lowercase letters, digits, hyphens, starts with a letter`);
|
|
204
|
+
if (/^(my first project|untitled|test|default|new project)$/i.test(name)) fail(`"${name}" is a placeholder; name the project after the product`);
|
|
205
|
+
let existing = await api("GET", `https://cloudresourcemanager.googleapis.com/v3/projects/${id}`).catch((e) => (e.status === 403 || e.status === 404 ? null : Promise.reject(e)));
|
|
206
|
+
if (existing && existing.state !== "ACTIVE") fail(`${id} exists but is ${existing.state}; pick another ID or restore it`);
|
|
207
|
+
if (!existing) {
|
|
208
|
+
if (!org && !folder) {
|
|
209
|
+
const orgs = (await api("POST", "https://cloudresourcemanager.googleapis.com/v1/organizations:search", {}).catch(() => ({}))).organizations ?? [];
|
|
210
|
+
if (orgs.length === 1) org = orgs[0].name.split("/")[1];
|
|
211
|
+
}
|
|
212
|
+
const parent = folder ? `folders/${folder}` : org ? `organizations/${org}` : undefined;
|
|
213
|
+
console.log(`creating ${id} "${name}"${parent ? ` under ${parent}` : ""}…`);
|
|
214
|
+
try {
|
|
215
|
+
await wait(await api("POST", "https://cloudresourcemanager.googleapis.com/v3/projects", { projectId: id, displayName: name, ...(parent ? { parent } : {}) }), "https://cloudresourcemanager.googleapis.com/v3");
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (/already exists|ALREADY_EXISTS/i.test(error.message)) fail(`project ID ${id} is taken globally (by someone else); choose another, e.g. ${id}-app`);
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
existing = await api("GET", `https://cloudresourcemanager.googleapis.com/v3/projects/${id}`);
|
|
221
|
+
} else if (existing.displayName !== name) {
|
|
222
|
+
console.log(`renaming ${id}: "${existing.displayName}" → "${name}"`);
|
|
223
|
+
await wait(await api("PATCH", `https://cloudresourcemanager.googleapis.com/v3/projects/${id}?updateMask=displayName`, { displayName: name }), "https://cloudresourcemanager.googleapis.com/v3");
|
|
224
|
+
existing = await api("GET", `https://cloudresourcemanager.googleapis.com/v3/projects/${id}`);
|
|
225
|
+
}
|
|
226
|
+
console.log(`✓ project ${existing.projectId} "${existing.displayName}" (number ${existing.name.split("/")[1]}, parent ${existing.parent ?? "none"})`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function serviceIds(list) {
|
|
230
|
+
return list.map((a) => API_ALIASES[a] ?? (a.includes(".") ? a : `${a}.googleapis.com`));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function enable(project, apis) {
|
|
234
|
+
if (!project || !apis.length) fail("--project and --apis are required");
|
|
235
|
+
const ids = serviceIds(apis);
|
|
236
|
+
await wait(await api("POST", `https://serviceusage.googleapis.com/v1/projects/${project}/services:batchEnable`, { serviceIds: ids }), "https://serviceusage.googleapis.com/v1");
|
|
237
|
+
console.log(`✓ enabled on ${project}: ${ids.join(", ")}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function audit(project) {
|
|
241
|
+
if (!project) fail("--project is required");
|
|
242
|
+
const p = await api("GET", `https://cloudresourcemanager.googleapis.com/v3/projects/${project}`);
|
|
243
|
+
console.log(`project: ${p.projectId} name "${p.displayName}" state ${p.state} parent ${p.parent ?? "none"}`);
|
|
244
|
+
if (/^(my first project|untitled)/i.test(p.displayName) || p.displayName === p.projectId) {
|
|
245
|
+
console.log(` ! the display name is a placeholder; run ensure-project --id ${p.projectId} --name "<Product>"`);
|
|
246
|
+
}
|
|
247
|
+
const services = [];
|
|
248
|
+
let pageToken;
|
|
249
|
+
do {
|
|
250
|
+
const page = await api("GET", `https://serviceusage.googleapis.com/v1/projects/${project}/services?filter=state:ENABLED&pageSize=200${pageToken ? `&pageToken=${pageToken}` : ""}`);
|
|
251
|
+
services.push(...(page.services ?? []).map((s) => s.config.name));
|
|
252
|
+
pageToken = page.nextPageToken;
|
|
253
|
+
} while (pageToken);
|
|
254
|
+
console.log(`apis: ${services.filter((s) => !/^(bigquery|cloudapis|cloudtrace|datastore|logging|monitoring|servicemanagement|serviceusage|sql-component|storage-api|storage-component|cloudasset|dataform|dataplex|analyticshub|bigquery\w*)\./.test(s)).join(", ") || "(only defaults)"}`);
|
|
255
|
+
console.log(`console: https://console.cloud.google.com/auth/overview?project=${project}`);
|
|
256
|
+
console.log("note: OAuth branding and clients have no public API; check them in the Console or with check-client.");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function oauthPlan(o) {
|
|
260
|
+
const project = o.one("project");
|
|
261
|
+
const name = o.one("name");
|
|
262
|
+
const home = o.one("home");
|
|
263
|
+
const support = o.one("support");
|
|
264
|
+
const redirects = o.many("redirect");
|
|
265
|
+
if (!project || !name || !home || !support || !redirects.length) fail("--project, --name, --home, --support and at least one --redirect are required");
|
|
266
|
+
const homeUrl = new URL(home);
|
|
267
|
+
const domain = o.one("domain") ?? homeUrl.hostname.split(".").slice(-2).join(".");
|
|
268
|
+
const privacy = o.one("privacy") ?? `${homeUrl.origin}/privacy`;
|
|
269
|
+
const terms = o.one("terms") ?? `${homeUrl.origin}/terms`;
|
|
270
|
+
const scopes = ["openid", "email", "profile", ...o.many("scopes")];
|
|
271
|
+
const resolved = scopes.map((s) => [s, ...(SCOPES[s] ?? [s.startsWith("http") ? s : `https://www.googleapis.com/auth/${s}`, "unknown"])]);
|
|
272
|
+
const worst = resolved.some((r) => r[2] === "restricted") ? "restricted" : resolved.some((r) => r[2] === "sensitive") ? "sensitive" : "basic";
|
|
273
|
+
const bad = redirects.filter((r) => {
|
|
274
|
+
try {
|
|
275
|
+
const u = new URL(r);
|
|
276
|
+
return !(u.protocol === "https:" || (u.protocol === "http:" && /^(localhost|127\.0\.0\.1|\[::1\])$/.test(u.hostname)));
|
|
277
|
+
} catch {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
if (bad.length) fail(`redirect URIs must be https (or http://localhost): ${bad.join(", ")}`);
|
|
282
|
+
const base = "https://console.cloud.google.com/auth";
|
|
283
|
+
const q = `?project=${project}`;
|
|
284
|
+
console.log(`OAuth setup for "${name}" in project ${project}. None of this has a public API: do it in the Console`);
|
|
285
|
+
console.log(`(Claude in Chrome / the user's browser), signed in as an owner of ${project}. Check the account chip first.\n`);
|
|
286
|
+
console.log(`1. Branding ${base}/branding${q}`);
|
|
287
|
+
console.log(` App name ............ ${name} ← exactly what users see: "Sign in to continue to ${name}"`);
|
|
288
|
+
console.log(` User support email .. ${support}`);
|
|
289
|
+
console.log(` App logo ............ optional (a logo sends the app to brand verification; skip it until you verify)`);
|
|
290
|
+
console.log(` Application home .... ${homeUrl.origin}`);
|
|
291
|
+
console.log(` Privacy policy ...... ${privacy}`);
|
|
292
|
+
console.log(` Terms of service .... ${terms}`);
|
|
293
|
+
console.log(` Authorized domains .. ${domain}`);
|
|
294
|
+
console.log(` Developer contact ... ${support}`);
|
|
295
|
+
console.log(`2. Audience ${base}/audience${q}`);
|
|
296
|
+
console.log(` User type External (Internal only lets accounts in your Workspace sign in).`);
|
|
297
|
+
console.log(worst === "basic"
|
|
298
|
+
? " Publishing status: Publish app → In production (basic scopes need no review)."
|
|
299
|
+
: ` Keep "Testing" and add test users until verified; in Testing, refresh tokens expire after 7 days and only test users can sign in.`);
|
|
300
|
+
console.log(`3. Data access ${base}/scopes${q}`);
|
|
301
|
+
for (const [short, full, kind] of resolved) console.log(` ${kind.padEnd(10)} ${full === "openid" ? "openid" : full}${short !== full ? ` (${short})` : ""}`);
|
|
302
|
+
if (worst === "restricted") console.log(" Restricted scopes need Google verification + a CASA security assessment before > 100 users / non-test users. Prefer narrower scopes if they work.");
|
|
303
|
+
else if (worst === "sensitive") console.log(" Sensitive scopes need Google verification (a demo video + justification) before the unverified-app warning goes away.");
|
|
304
|
+
console.log(`4. Clients ${base}/clients/create${q}`);
|
|
305
|
+
console.log(` Application type: Web application Name: ${name} web`);
|
|
306
|
+
for (const r of redirects) console.log(` Authorized redirect URI: ${r}`);
|
|
307
|
+
console.log(" Create, then copy the client ID and secret at once (the secret is not shown again; download the JSON).");
|
|
308
|
+
console.log("5. Store, never print. e.g.");
|
|
309
|
+
console.log(" fly secrets set -a <app> GOOGLE_CLIENT_ID=… GOOGLE_CLIENT_SECRET=… | vercel env add GOOGLE_CLIENT_SECRET production");
|
|
310
|
+
console.log("6. Verify from outside:");
|
|
311
|
+
console.log(` node ${process.argv[1]} check-client --client-id <ID> --expect-name "${name}"${redirects.map((r) => ` --redirect ${r}`).join("")}`);
|
|
312
|
+
console.log(" then do one real sign-in in a browser and confirm the consent screen says the right name.");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function pageText(html) {
|
|
316
|
+
return html
|
|
317
|
+
.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>/g, " ")
|
|
318
|
+
.replace(/<[^>]+>/g, " ")
|
|
319
|
+
.replace(/'|'/g, "'")
|
|
320
|
+
.replace(/"/g, '"')
|
|
321
|
+
.replace(/&/g, "&")
|
|
322
|
+
.replace(/\s+/g, " ");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// What a signed-out user's browser gets for this client + redirect. Google
|
|
326
|
+
// validates the client and redirect before asking anyone to sign in, and the
|
|
327
|
+
// sign-in page names the app from the consent screen's Branding.
|
|
328
|
+
async function probe(clientId, redirect, scopes) {
|
|
329
|
+
const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
|
|
330
|
+
url.search = new URLSearchParams({ client_id: clientId, redirect_uri: redirect, response_type: "code", scope: scopes.join(" "), prompt: "select_account" }).toString();
|
|
331
|
+
const response = await fetch(url, { redirect: "follow", headers: { "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36", "accept-language": "en-US" } });
|
|
332
|
+
const text = pageText(await response.text());
|
|
333
|
+
const error = text.match(/Error 400: (\w+)/)?.[1] ?? text.match(/Error 401: (\w+)/)?.[1] ?? text.match(/Error 403: (\w+)/)?.[1] ?? (response.url.includes("/signin/oauth/error") ? "unknown_error" : null);
|
|
334
|
+
const appName = text.match(/to continue to ([^.]+?)(?:\s+(?:Email|Forgot|Not your|Create account|Next)\b|$)/)?.[1]?.trim() ?? null;
|
|
335
|
+
return { error, appName, finalUrl: response.url };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function checkClient(o) {
|
|
339
|
+
const clientId = o.one("client-id");
|
|
340
|
+
const redirects = o.many("redirect");
|
|
341
|
+
const expect = o.one("expect-name");
|
|
342
|
+
const scopes = ["openid", "email", ...o.many("scopes").map((s) => SCOPES[s]?.[0] ?? s)];
|
|
343
|
+
if (!clientId || !redirects.length) fail("--client-id and at least one --redirect are required");
|
|
344
|
+
if (!/\.apps\.googleusercontent\.com$/.test(clientId)) fail("that doesn't look like a Google OAuth client ID (…apps.googleusercontent.com)");
|
|
345
|
+
let ok = true;
|
|
346
|
+
for (const redirect of redirects) {
|
|
347
|
+
const r = await probe(clientId, redirect, scopes);
|
|
348
|
+
if (r.error) {
|
|
349
|
+
ok = false;
|
|
350
|
+
const hint = {
|
|
351
|
+
redirect_uri_mismatch: `add exactly ${redirect} to the client's Authorized redirect URIs (scheme, host, path and trailing slash must match)`,
|
|
352
|
+
invalid_client: "the client ID is wrong, or the client was deleted",
|
|
353
|
+
deleted_client: "this client was deleted; create a new one",
|
|
354
|
+
invalid_request: "the request is malformed (often a non-https redirect or a scope typo)",
|
|
355
|
+
access_denied: "the app is blocked (org policy, or Testing mode without this user as a test user)",
|
|
356
|
+
org_internal: "the consent screen is Internal; switch Audience to External",
|
|
357
|
+
}[r.error] ?? "see the Console's client page";
|
|
358
|
+
console.log(`✗ ${redirect}: ${r.error}. Fix: ${hint}.`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (!r.appName) {
|
|
362
|
+
console.log(`? ${redirect}: accepted, but the app name wasn't readable (Google changed the page?). Check by signing in once.`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const nameOk = !expect || r.appName.toLowerCase() === expect.toLowerCase();
|
|
366
|
+
if (!nameOk) ok = false;
|
|
367
|
+
console.log(`${nameOk ? "✓" : "✗"} ${redirect}: accepted; Google shows "continue to ${r.appName}"${nameOk ? "" : `, expected "${expect}". Fix the App name on the Branding page (it is not the project name).`}`);
|
|
368
|
+
}
|
|
369
|
+
if (!ok) process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
const o = parseArgs(process.argv.slice(2));
|
|
375
|
+
try {
|
|
376
|
+
switch (o.command) {
|
|
377
|
+
case "whoami": await whoami(); break;
|
|
378
|
+
case "projects": await projects(o.one("filter")); break;
|
|
379
|
+
case "ensure-project": await ensureProject(o.one("id"), o.one("name"), o.one("org"), o.one("folder")); break;
|
|
380
|
+
case "enable": await enable(o.one("project"), o.many("apis")); break;
|
|
381
|
+
case "audit": await audit(o.one("project")); break;
|
|
382
|
+
case "oauth-plan": oauthPlan(o); break;
|
|
383
|
+
case "check-client": await checkClient(o); break;
|
|
384
|
+
default:
|
|
385
|
+
console.log(readFileSync(new URL(import.meta.url), "utf8").split("\n").slice(1, 19).map((l) => l.replace(/^\/\/ ?/, "")).join("\n"));
|
|
386
|
+
process.exit(o.command ? 1 : 0);
|
|
387
|
+
}
|
|
388
|
+
} catch (error) {
|
|
389
|
+
fail(error.message);
|
|
390
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type AgentSkillTarget = {
|
|
2
|
+
agent: "claude" | "codex";
|
|
3
|
+
dir: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function agentSkillsSourceDir(): string;
|
|
6
|
+
export declare function agentSkillTargets(home?: string, env?: NodeJS.ProcessEnv): AgentSkillTarget[];
|
|
7
|
+
export type InstallResult = {
|
|
8
|
+
installed: string[];
|
|
9
|
+
updatedFiles: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function installAgentSkills(options?: {
|
|
12
|
+
sourceDir?: string;
|
|
13
|
+
targets?: AgentSkillTarget[];
|
|
14
|
+
}): Promise<InstallResult>;
|
|
15
|
+
/** Best effort: a read-only home or odd permissions must never block a run. */
|
|
16
|
+
export declare function ensureAgentSkills(env?: NodeJS.ProcessEnv): Promise<void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
export function agentSkillsSourceDir() {
|
|
6
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "assets", "skills");
|
|
7
|
+
}
|
|
8
|
+
export function agentSkillTargets(home = os.homedir(), env = process.env) {
|
|
9
|
+
return [
|
|
10
|
+
{ agent: "claude", dir: path.join(env.CLAUDE_CONFIG_DIR || path.join(home, ".claude"), "skills") },
|
|
11
|
+
{ agent: "codex", dir: path.join(env.CODEX_HOME || path.join(home, ".codex"), "skills") },
|
|
12
|
+
];
|
|
13
|
+
}
|
|
14
|
+
async function listFiles(root, rel = "") {
|
|
15
|
+
const entries = await fsp.readdir(path.join(root, rel), { withFileTypes: true });
|
|
16
|
+
const files = [];
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const child = path.join(rel, entry.name);
|
|
19
|
+
if (entry.isDirectory())
|
|
20
|
+
files.push(...(await listFiles(root, child)));
|
|
21
|
+
else if (entry.isFile())
|
|
22
|
+
files.push(child);
|
|
23
|
+
}
|
|
24
|
+
return files;
|
|
25
|
+
}
|
|
26
|
+
export async function installAgentSkills(options = {}) {
|
|
27
|
+
const sourceDir = options.sourceDir ?? agentSkillsSourceDir();
|
|
28
|
+
const targets = options.targets ?? agentSkillTargets();
|
|
29
|
+
let skills;
|
|
30
|
+
try {
|
|
31
|
+
skills = (await fsp.readdir(sourceDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { installed: [], updatedFiles: 0 };
|
|
35
|
+
}
|
|
36
|
+
const installed = [];
|
|
37
|
+
let updatedFiles = 0;
|
|
38
|
+
for (const skill of skills) {
|
|
39
|
+
const files = await listFiles(path.join(sourceDir, skill));
|
|
40
|
+
if (!files.includes("SKILL.md"))
|
|
41
|
+
continue;
|
|
42
|
+
for (const target of targets) {
|
|
43
|
+
const dest = path.join(target.dir, `rudder-${skill}`);
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
const from = path.join(sourceDir, skill, file);
|
|
46
|
+
const to = path.join(dest, file);
|
|
47
|
+
const content = await fsp.readFile(from);
|
|
48
|
+
const current = await fsp.readFile(to).catch(() => null);
|
|
49
|
+
if (current && current.equals(content))
|
|
50
|
+
continue;
|
|
51
|
+
await fsp.mkdir(path.dirname(to), { recursive: true });
|
|
52
|
+
await fsp.writeFile(to, content);
|
|
53
|
+
if (file.startsWith(`scripts${path.sep}`))
|
|
54
|
+
await fsp.chmod(to, 0o755);
|
|
55
|
+
updatedFiles++;
|
|
56
|
+
}
|
|
57
|
+
installed.push(`${target.agent}:rudder-${skill}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { installed, updatedFiles };
|
|
61
|
+
}
|
|
62
|
+
/** Best effort: a read-only home or odd permissions must never block a run. */
|
|
63
|
+
export async function ensureAgentSkills(env = process.env) {
|
|
64
|
+
if (env.RUDDER_DISABLE_AGENT_SKILLS === "1")
|
|
65
|
+
return;
|
|
66
|
+
await installAgentSkills().catch(() => undefined);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=agent-skills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-skills.js","sourceRoot":"","sources":["../src/agent-skills.ts"],"names":[],"mappings":"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"}
|
package/dist/attach-session.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type SessionOptions = {
|
|
|
23
23
|
};
|
|
24
24
|
export declare const KITTY_KEYBOARD_PUSH = "\u001B[>5u";
|
|
25
25
|
export declare const KITTY_KEYBOARD_POP = "\u001B[<u";
|
|
26
|
+
export declare const KEYBOARD_RESET = "\u001B[<99u\u001B[=0;1u\u001B[>4m\u001B[?1004l";
|
|
26
27
|
export declare function containsCtrlC(data: Buffer): boolean;
|
|
27
28
|
export declare class AttachSession {
|
|
28
29
|
/** Sent as `?client=`: the relay lets this terminal take its own place back. */
|