requestshield 0.1.0 → 0.1.3
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 +37 -6
- package/package.json +3 -1
- package/skills/requestshield/SKILL.md +302 -39
- package/skills/requestshield/assets/AGENTS.codex.md +62 -0
- package/skills/requestshield/references/backend-java-core.md +128 -0
- package/skills/requestshield/references/backend-spring-boot.md +145 -0
- package/skills/requestshield/references/browser-manual.md +210 -0
- package/skills/requestshield/references/browser-seamless.md +164 -0
- package/skills/requestshield/references/cli.md +182 -0
- package/skills/requestshield/references/integration-planning.md +389 -0
- package/skills/requestshield/references/troubleshooting.md +118 -0
- package/src/agent-detector.mjs +74 -0
- package/src/args.mjs +17 -13
- package/src/cli.mjs +1 -1
- package/src/commands/agent-setup.mjs +109 -15
- package/src/main.mjs +24 -24
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Work outward from the request, not from the code. Most reports of "RequestShield is
|
|
4
|
+
broken" are one of four things: the browser never attached a token, CORS stripped the
|
|
5
|
+
header, the backend never verified it, or one token was verified twice.
|
|
6
|
+
|
|
7
|
+
## First three checks
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
requestshield server # platform healthy or degraded?
|
|
11
|
+
requestshield apps get <app-key> # status: ready (no traffic yet) or active?
|
|
12
|
+
requestshield credentials status <app-key> # provisioned and active?
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`status: ready` means the platform has never seen traffic for that key — the integration
|
|
16
|
+
is not wired up yet, so debugging backend enforcement logic is premature. Start at the
|
|
17
|
+
browser.
|
|
18
|
+
|
|
19
|
+
Run `credentials status` even when the pre-edit Credentials check in
|
|
20
|
+
`integration-planning.md` passed during the install. That check greps the **local
|
|
21
|
+
shell**; it says nothing about what the deployed backend actually receives, and a secret
|
|
22
|
+
can be rotated, or missed by the deploy environment, after it passed. This is the check
|
|
23
|
+
that answers `consume_unauthorized`.
|
|
24
|
+
|
|
25
|
+
## Reason codes
|
|
26
|
+
|
|
27
|
+
`result.getReason().getCode()` returns a stable code, safe to log. Use codes to explain
|
|
28
|
+
a decision, never to amend one.
|
|
29
|
+
|
|
30
|
+
The table below is a working list, not the authoritative one — the customer
|
|
31
|
+
documentation owns that. Check a code against it before quoting a meaning to the user,
|
|
32
|
+
and treat a code that is not listed here as unrecognised rather than guessing from its
|
|
33
|
+
name.
|
|
34
|
+
|
|
35
|
+
| Code | Means | Usual cause |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `ok` | Verified and consumed | — |
|
|
38
|
+
| `missing_token` | No token reached the backend | Script tag absent or loaded after the request; Seamless path mismatch (case, trailing slash, wrong origin); an uncovered transport (form POST, sendBeacon, sync XHR, `no-cors`); a Manual call site that never attaches the header; CORS not allowing `X-IntelliFend-Token`; or CSP missing `worker-src blob:` so `getToken()` returns empty. |
|
|
39
|
+
| `unknown_customer` | App Key not recognised | Typo, deactivated key, or browser and backend configured with different App Keys. |
|
|
40
|
+
| `token_tampered` | Token failed integrity checks | Something modified the value in transit — a proxy rewriting headers, or application code trimming or re-encoding it. Pass the value through unchanged. |
|
|
41
|
+
| `token_expired` | Token too old | The page held a token instead of obtaining a fresh one per request, or a very slow round trip. |
|
|
42
|
+
| `token_replayed` | Already consumed | The same token verified twice: a retry reusing the first token, or two verification paths on one request (`@RequestShieldProtected` plus a manual `verify()`, or a filter plus a controller). |
|
|
43
|
+
| `consume_unauthorized` | Backend credentials rejected | API Secret wrong, rotated, or not injected — check `credentials status` and the environment variable. |
|
|
44
|
+
| `invalid_request` | Malformed verification request | Usually a mis-set `challenge-server-url`. |
|
|
45
|
+
| `not_found` | Token unknown to the platform | Cross-environment mismatch — a staging page's token verified against production, or the reverse. |
|
|
46
|
+
| `service_unavailable` | Platform unreachable | Check `requestshield server`. See degraded behaviour below. |
|
|
47
|
+
|
|
48
|
+
## "It blocks everything"
|
|
49
|
+
|
|
50
|
+
Almost always `missing_token` or `unknown_customer`. Confirm in this order — each step is
|
|
51
|
+
cheap and rules out a whole class:
|
|
52
|
+
|
|
53
|
+
1. Open the network tab. Does the protected request carry `X-IntelliFend-Token` with a
|
|
54
|
+
non-empty value?
|
|
55
|
+
2. **Header absent** → browser half. Seamless: compare the requested URL against
|
|
56
|
+
`data-protect` character for character — case and trailing slash are significant — and
|
|
57
|
+
check the transport is `fetch` or async XHR from page scope. Manual: find the call
|
|
58
|
+
site, and every retry path.
|
|
59
|
+
3. **Header absent on a cross-origin request only** → CORS. The API's
|
|
60
|
+
`Access-Control-Allow-Headers` must include `X-IntelliFend-Token`, or the browser
|
|
61
|
+
strips it before it ever leaves.
|
|
62
|
+
4. **Header present but empty** → the SDK ran and could not obtain a token. Check the CSP
|
|
63
|
+
includes `worker-src 'self' blob:` and that `connect-src` allows the challenge host,
|
|
64
|
+
then check `requestshield server`. (In Manual mode an empty token means the header
|
|
65
|
+
should be omitted, not sent empty — see `browser-manual.md`.)
|
|
66
|
+
5. **Header present and populated** → backend half. Compare the App Key on both sides
|
|
67
|
+
character for character, then confirm the secret is actually injected at runtime.
|
|
68
|
+
|
|
69
|
+
## "It blocks nothing"
|
|
70
|
+
|
|
71
|
+
Enumerate rather than assuming the browser is at fault:
|
|
72
|
+
|
|
73
|
+
- The endpoint is in `MONITOR` mode — evaluating and always continuing. Protection-shaped,
|
|
74
|
+
not protection.
|
|
75
|
+
- The decision is computed but only logged, never enforced.
|
|
76
|
+
- Verification happens after the mutation has already been performed.
|
|
77
|
+
- Application code overrides `isAllowed()` from a reason code — a separate decision table
|
|
78
|
+
in application code removes the protection.
|
|
79
|
+
- The backend never verifies at all: a browser-only install. Grep for the client or the
|
|
80
|
+
annotation before trusting a claim that the backend is wired up.
|
|
81
|
+
- Results are `isDegraded()` — the platform was unreachable and requests were allowed by
|
|
82
|
+
the availability policy.
|
|
83
|
+
|
|
84
|
+
## Degraded results are allowed by design
|
|
85
|
+
|
|
86
|
+
When the Challenge Server cannot be reached, `verify()` returns a result that is
|
|
87
|
+
**allowed** and **degraded**. Availability is deliberately chosen over enforcement so a
|
|
88
|
+
platform outage cannot take down a customer's checkout flow.
|
|
89
|
+
|
|
90
|
+
Communicate the consequence clearly: during a degraded window, traffic passes without
|
|
91
|
+
real verification. Log the degraded rate as its own metric so it is visible, and check
|
|
92
|
+
`requestshield server` when it rises. Never "fix" a degraded result by overriding
|
|
93
|
+
`isAllowed()` — that converts a platform incident into a customer-facing outage.
|
|
94
|
+
|
|
95
|
+
## Confirming traffic reaches the platform
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
requestshield challenge volume <app-key> --from 2026-08-01T00:00:00Z \
|
|
99
|
+
--to 2026-08-27T23:59:59Z --granularity day
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Challenge volume is runtime evidence that the browser/platform half is active. It does
|
|
103
|
+
not prove that the backend enforces a blocking decision. Report static correctness,
|
|
104
|
+
platform traffic, and backend enforcement separately. Use the controlled negative test
|
|
105
|
+
in `SKILL.md` to prove enforcement.
|
|
106
|
+
|
|
107
|
+
## Secret exposure
|
|
108
|
+
|
|
109
|
+
If the API Secret appears in a repo, a log, a build artifact, or a chat transcript,
|
|
110
|
+
rotate it — deleting the line does not un-expose it:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
requestshield app key rotate --app-key <app-key>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The App Key is unchanged, so no browser edit is needed. Deploy the new secret to backend
|
|
117
|
+
secret storage; the previous one is deactivated automatically. Warn the user that the new
|
|
118
|
+
secret is displayed once.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { access } from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detect supported agents from their executable or local configuration.
|
|
10
|
+
*
|
|
11
|
+
* @param {{ env?: NodeJS.ProcessEnv, homeDir?: string }} [options]
|
|
12
|
+
* @returns {Promise<Array<"codex" | "claude">>}
|
|
13
|
+
*/
|
|
14
|
+
export async function detectAgents(options = {}) {
|
|
15
|
+
const env = options.env ?? process.env;
|
|
16
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
17
|
+
const codexHome = env.CODEX_HOME ?? path.join(homeDir, ".codex");
|
|
18
|
+
|
|
19
|
+
// Executables prove the CLI is on PATH. Configuration files cover desktop
|
|
20
|
+
// installs and agents that were previously configured but are not on PATH.
|
|
21
|
+
const [codexCommand, claudeCommand, codexConfig, claudeConfig, claudeState] =
|
|
22
|
+
await Promise.all([
|
|
23
|
+
commandExists("codex", env),
|
|
24
|
+
commandExists("claude", env),
|
|
25
|
+
pathExists(codexHome),
|
|
26
|
+
pathExists(path.join(homeDir, ".claude")),
|
|
27
|
+
pathExists(path.join(homeDir, ".claude.json")),
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/** @type {Array<"codex" | "claude">} */
|
|
31
|
+
const detected = [];
|
|
32
|
+
if (codexCommand || codexConfig) detected.push("codex");
|
|
33
|
+
if (claudeCommand || claudeConfig || claudeState) detected.push("claude");
|
|
34
|
+
return detected;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Inspect PATH without starting the coding agent.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} command
|
|
41
|
+
* @param {NodeJS.ProcessEnv} env
|
|
42
|
+
*/
|
|
43
|
+
async function commandExists(command, env) {
|
|
44
|
+
const searchPath = env.PATH ?? env.Path ?? "";
|
|
45
|
+
const extensions = process.platform === "win32"
|
|
46
|
+
? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")
|
|
47
|
+
: [""];
|
|
48
|
+
|
|
49
|
+
for (const directory of searchPath.split(path.delimiter)) {
|
|
50
|
+
if (!directory) continue;
|
|
51
|
+
for (const extension of extensions) {
|
|
52
|
+
try {
|
|
53
|
+
await access(
|
|
54
|
+
path.join(directory, `${command}${extension}`),
|
|
55
|
+
process.platform === "win32" ? constants.F_OK : constants.X_OK,
|
|
56
|
+
);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
// Continue through the remaining executable candidates.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @param {string} target */
|
|
67
|
+
async function pathExists(target) {
|
|
68
|
+
try {
|
|
69
|
+
await access(target);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/args.mjs
CHANGED
|
@@ -7,7 +7,9 @@ const HELP = `RequestShield customer CLI
|
|
|
7
7
|
Usage:
|
|
8
8
|
requestshield signin
|
|
9
9
|
requestshield keys create [--yes]
|
|
10
|
-
requestshield agent setup
|
|
10
|
+
requestshield agent setup [--force]
|
|
11
|
+
requestshield agent setup --codex [--force]
|
|
12
|
+
requestshield agent setup --claude [--force]
|
|
11
13
|
requestshield --help
|
|
12
14
|
requestshield --version`;
|
|
13
15
|
|
|
@@ -16,7 +18,7 @@ Usage:
|
|
|
16
18
|
* | { command: "version" }
|
|
17
19
|
* | { command: "signin" }
|
|
18
20
|
* | { command: "keys-create", yes: boolean }
|
|
19
|
-
* | { command: "agent-setup", agent
|
|
21
|
+
* | { command: "agent-setup", agent?: "codex" | "claude", force: boolean }} ParsedArgs
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
/** @param {string[]} argv @returns {ParsedArgs} */
|
|
@@ -37,28 +39,30 @@ export function parseArgs(argv) {
|
|
|
37
39
|
}
|
|
38
40
|
if (argv[0] === "agent" && argv[1] === "setup") {
|
|
39
41
|
const rest = argv.slice(2);
|
|
40
|
-
/** @type {
|
|
42
|
+
/** @type {"codex" | "claude" | undefined} */
|
|
41
43
|
let agent;
|
|
42
44
|
let force = false;
|
|
43
45
|
for (let index = 0; index < rest.length; index++) {
|
|
44
46
|
const arg = rest[index];
|
|
45
47
|
if (arg === "--force") {
|
|
46
48
|
force = true;
|
|
47
|
-
} else if (arg === "--
|
|
48
|
-
agent
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
} else if (arg === "--codex" || arg === "--claude") {
|
|
50
|
+
if (agent !== undefined) {
|
|
51
|
+
throw new CliError("Only one of --codex or --claude may be provided", {
|
|
52
|
+
code: "INVALID_AGENT",
|
|
53
|
+
exitCode: 2,
|
|
54
|
+
});
|
|
51
55
|
}
|
|
56
|
+
agent = arg === "--codex" ? "codex" : "claude";
|
|
52
57
|
} else {
|
|
53
58
|
throw new CliError(`Unknown option: ${arg}`, { exitCode: 2 });
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
return { command: "agent-setup", agent: "codex", force };
|
|
61
|
+
return {
|
|
62
|
+
command: "agent-setup",
|
|
63
|
+
...(agent ? { agent } : {}),
|
|
64
|
+
force,
|
|
65
|
+
};
|
|
62
66
|
}
|
|
63
67
|
throw new CliError(`Unknown command.\n\n${HELP}`, { exitCode: 2 });
|
|
64
68
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import packageJson from "../package.json" with { type: "json" };
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* @param {string[]} argv
|
|
14
|
-
* @param {{ log?: (message: string) => void, api?: ManagementApiClient, sessions?: SessionStore, env?: NodeJS.ProcessEnv, homeDir?: string, sourceDir?: string, wait?: (milliseconds: number) => Promise<unknown>, confirm?: () => Promise<boolean> }} [deps]
|
|
14
|
+
* @param {{ log?: (message: string) => void, api?: ManagementApiClient, sessions?: SessionStore, env?: NodeJS.ProcessEnv, homeDir?: string, sourceDir?: string, executablePath?: string, detectAgents?: () => Promise<Array<"codex" | "claude">>, selectAgent?: (detected: Array<"codex" | "claude">) => Promise<"codex" | "claude">, wait?: (milliseconds: number) => Promise<unknown>, confirm?: () => Promise<boolean> }} [deps]
|
|
15
15
|
*/
|
|
16
16
|
export async function run(argv, deps = {}) {
|
|
17
17
|
const parsed = parseArgs(argv);
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { cp, lstat, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { cp, lstat, mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
7
8
|
import { CliError } from "../errors.mjs";
|
|
9
|
+
import { detectAgents } from "../agent-detector.mjs";
|
|
8
10
|
import { getAsset, isSea } from "node:sea";
|
|
9
11
|
|
|
10
12
|
const skillAsset = "requestshield-skill.md";
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
|
-
* @param {{ agent
|
|
14
|
-
* @param {{ env?: NodeJS.ProcessEnv, homeDir?: string, log: (message: string) => void, sourceDir?: string }} deps
|
|
15
|
+
* @param {{ agent?: "codex" | "claude", force: boolean }} options
|
|
16
|
+
* @param {{ env?: NodeJS.ProcessEnv, homeDir?: string, log: (message: string) => void, sourceDir?: string, executablePath?: string, detectAgents?: () => Promise<Array<"codex" | "claude">>, selectAgent?: (detected: Array<"codex" | "claude">) => Promise<"codex" | "claude"> }} deps
|
|
15
17
|
*/
|
|
16
18
|
export async function setupAgent(options, deps) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
19
|
+
const agent = await resolveAgent(options.agent, deps);
|
|
20
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
21
|
+
|
|
22
|
+
// Codex and Claude discover personal skills from different directories.
|
|
23
|
+
const skillsRoot = agent === "codex"
|
|
24
|
+
? path.join(homeDir, ".agents", "skills")
|
|
25
|
+
: path.join(homeDir, ".claude", "skills");
|
|
23
26
|
const destination = path.join(skillsRoot, "requestshield");
|
|
24
27
|
await mkdir(skillsRoot, { recursive: true });
|
|
25
28
|
|
|
@@ -31,17 +34,19 @@ export async function setupAgent(options, deps) {
|
|
|
31
34
|
});
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
// Copy into a staging directory so an interrupted copy cannot leave a
|
|
38
|
+
// partially installed skill at the final destination.
|
|
34
39
|
const staging = path.join(skillsRoot, `.requestshield-${randomUUID()}.tmp`);
|
|
35
40
|
const backup = `${destination}.${randomUUID()}.backup`;
|
|
36
41
|
try {
|
|
37
42
|
if (deps.sourceDir) {
|
|
38
|
-
//
|
|
43
|
+
// Tests and alternate packagers can provide an explicit skill source.
|
|
39
44
|
await cp(deps.sourceDir, staging, {
|
|
40
45
|
recursive: true,
|
|
41
46
|
errorOnExist: true,
|
|
42
47
|
});
|
|
43
48
|
} else if (isSea()) {
|
|
44
|
-
//
|
|
49
|
+
// A single-executable build reads the skill from its embedded assets.
|
|
45
50
|
await mkdir(staging);
|
|
46
51
|
|
|
47
52
|
await writeFile(
|
|
@@ -49,11 +54,14 @@ export async function setupAgent(options, deps) {
|
|
|
49
54
|
Buffer.from(getAsset(skillAsset)),
|
|
50
55
|
);
|
|
51
56
|
} else {
|
|
52
|
-
//
|
|
57
|
+
// Source, npm, and on-disk CJS builds keep the skill beside the program.
|
|
53
58
|
//
|
|
54
59
|
// src/main.mjs -> ../skills/requestshield
|
|
55
60
|
// dist/requestshield.cjs -> ../skills/requestshield
|
|
56
|
-
|
|
61
|
+
// npm link and global npm installs expose a symlink in their bin folder.
|
|
62
|
+
// Follow it before resolving the adjacent packaged skills directory.
|
|
63
|
+
const executable = await realpath(deps.executablePath ?? process.argv[1]);
|
|
64
|
+
const executableDir = path.dirname(executable);
|
|
57
65
|
const source = path.resolve(
|
|
58
66
|
executableDir,
|
|
59
67
|
"../skills/requestshield",
|
|
@@ -64,6 +72,8 @@ export async function setupAgent(options, deps) {
|
|
|
64
72
|
errorOnExist: true,
|
|
65
73
|
});
|
|
66
74
|
}
|
|
75
|
+
|
|
76
|
+
// Keep the old installation recoverable until the staged copy is in place.
|
|
67
77
|
if (exists) await rename(destination, backup);
|
|
68
78
|
await rename(staging, destination);
|
|
69
79
|
if (exists) await rm(backup, { recursive: true, force: true });
|
|
@@ -75,8 +85,92 @@ export async function setupAgent(options, deps) {
|
|
|
75
85
|
}
|
|
76
86
|
throw error;
|
|
77
87
|
}
|
|
78
|
-
|
|
79
|
-
deps.log(
|
|
88
|
+
const agentLabel = agent === "codex" ? "Codex" : "Claude";
|
|
89
|
+
deps.log(`Installed RequestShield skill for ${agentLabel} at ${destination}`);
|
|
90
|
+
deps.log(`Restart ${agentLabel} or start a new task before using the skill.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Explicit selection bypasses detection. Automatic setup selects a single
|
|
95
|
+
* detected agent or asks the user when both agents are present.
|
|
96
|
+
*
|
|
97
|
+
* @param {"codex" | "claude" | undefined} requested
|
|
98
|
+
* @param {{ env?: NodeJS.ProcessEnv, homeDir?: string, detectAgents?: () => Promise<Array<"codex" | "claude">>, selectAgent?: (detected: Array<"codex" | "claude">) => Promise<"codex" | "claude"> }} deps
|
|
99
|
+
* @returns {Promise<"codex" | "claude">}
|
|
100
|
+
*/
|
|
101
|
+
async function resolveAgent(requested, deps) {
|
|
102
|
+
// A command-line choice is authoritative and avoids probing the machine.
|
|
103
|
+
if (requested) return requested;
|
|
104
|
+
|
|
105
|
+
const detected = deps.detectAgents
|
|
106
|
+
? await deps.detectAgents()
|
|
107
|
+
: await detectAgents({ env: deps.env, homeDir: deps.homeDir });
|
|
108
|
+
|
|
109
|
+
if (detected.length === 1) return detected[0];
|
|
110
|
+
if (detected.length === 0) {
|
|
111
|
+
throw new CliError(
|
|
112
|
+
[
|
|
113
|
+
"No supported coding agent detected.",
|
|
114
|
+
"",
|
|
115
|
+
"Choose one explicitly:",
|
|
116
|
+
" requestshield agent setup --codex",
|
|
117
|
+
" requestshield agent setup --claude",
|
|
118
|
+
].join("\n"),
|
|
119
|
+
{ code: "AGENT_NOT_FOUND", exitCode: 2 },
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Never choose silently when both agents are present. Interactive users get
|
|
124
|
+
// a prompt; non-interactive callers receive a stable conflict error.
|
|
125
|
+
const selectAgent = deps.selectAgent ?? promptForAgent;
|
|
126
|
+
const selected = await selectAgent(detected);
|
|
127
|
+
if (!detected.includes(selected)) {
|
|
128
|
+
throw new CliError(`Selected agent is not available: ${selected}`, {
|
|
129
|
+
code: "INVALID_AGENT",
|
|
130
|
+
exitCode: 2,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return selected;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Ask an interactive user which detected agent should receive the skill.
|
|
138
|
+
*
|
|
139
|
+
* @returns {Promise<"codex" | "claude">}
|
|
140
|
+
*/
|
|
141
|
+
async function promptForAgent() {
|
|
142
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
143
|
+
throw new CliError(
|
|
144
|
+
[
|
|
145
|
+
"Multiple coding agents detected.",
|
|
146
|
+
"",
|
|
147
|
+
"Choose one explicitly:",
|
|
148
|
+
" requestshield agent setup --codex",
|
|
149
|
+
" requestshield agent setup --claude",
|
|
150
|
+
].join("\n"),
|
|
151
|
+
{ code: "INSTALL_CONFLICT", exitCode: 2 },
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const prompt = createInterface({
|
|
156
|
+
input: process.stdin,
|
|
157
|
+
output: process.stdout,
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
process.stdout.write(
|
|
161
|
+
"Multiple coding agents detected.\n\n 1. Codex\n 2. Claude\n\n",
|
|
162
|
+
);
|
|
163
|
+
while (true) {
|
|
164
|
+
const answer = (await prompt.question("Select an agent [1-2]: "))
|
|
165
|
+
.trim()
|
|
166
|
+
.toLowerCase();
|
|
167
|
+
if (answer === "1" || answer === "codex") return "codex";
|
|
168
|
+
if (answer === "2" || answer === "claude") return "claude";
|
|
169
|
+
process.stdout.write("Enter 1 for Codex or 2 for Claude.\n");
|
|
170
|
+
}
|
|
171
|
+
} finally {
|
|
172
|
+
prompt.close();
|
|
173
|
+
}
|
|
80
174
|
}
|
|
81
175
|
|
|
82
176
|
/** @param {string} target */
|
package/src/main.mjs
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// @ts-check
|
|
3
|
-
|
|
4
|
-
import { run } from "./cli.mjs";
|
|
5
|
-
import { CliError } from "./errors.mjs";
|
|
6
|
-
|
|
7
|
-
async function main() {
|
|
8
|
-
try {
|
|
9
|
-
await run(process.argv.slice(2));
|
|
10
|
-
} catch (error) {
|
|
11
|
-
const message =
|
|
12
|
-
error instanceof Error
|
|
13
|
-
? error.message
|
|
14
|
-
: String(error);
|
|
15
|
-
|
|
16
|
-
console.error(`requestshield: ${message}`);
|
|
17
|
-
|
|
18
|
-
process.exitCode =
|
|
19
|
-
error instanceof CliError
|
|
20
|
-
? error.exitCode
|
|
21
|
-
: 1;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
void main();
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
import { run } from "./cli.mjs";
|
|
5
|
+
import { CliError } from "./errors.mjs";
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
try {
|
|
9
|
+
await run(process.argv.slice(2));
|
|
10
|
+
} catch (error) {
|
|
11
|
+
const message =
|
|
12
|
+
error instanceof Error
|
|
13
|
+
? error.message
|
|
14
|
+
: String(error);
|
|
15
|
+
|
|
16
|
+
console.error(`requestshield: ${message}`);
|
|
17
|
+
|
|
18
|
+
process.exitCode =
|
|
19
|
+
error instanceof CliError
|
|
20
|
+
? error.exitCode
|
|
21
|
+
: 1;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
void main();
|