@soku-ai/cli 0.1.0-alpha.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 +91 -0
- package/dist/auth/device.d.ts +36 -0
- package/dist/auth/device.d.ts.map +1 -0
- package/dist/auth/device.js +66 -0
- package/dist/auth/device.js.map +1 -0
- package/dist/auth/store.d.ts +11 -0
- package/dist/auth/store.d.ts.map +1 -0
- package/dist/auth/store.js +89 -0
- package/dist/auth/store.js.map +1 -0
- package/dist/commands/auth.d.ts +4 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +99 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/brand.d.ts +4 -0
- package/dist/commands/brand.d.ts.map +1 -0
- package/dist/commands/brand.js +53 -0
- package/dist/commands/brand.js.map +1 -0
- package/dist/commands/call.d.ts +4 -0
- package/dist/commands/call.d.ts.map +1 -0
- package/dist/commands/call.js +48 -0
- package/dist/commands/call.js.map +1 -0
- package/dist/commands/egress.d.ts +24 -0
- package/dist/commands/egress.d.ts.map +1 -0
- package/dist/commands/egress.js +197 -0
- package/dist/commands/egress.js.map +1 -0
- package/dist/commands/generated.d.ts +41 -0
- package/dist/commands/generated.d.ts.map +1 -0
- package/dist/commands/generated.js +106 -0
- package/dist/commands/generated.js.map +1 -0
- package/dist/commands/org.d.ts +4 -0
- package/dist/commands/org.d.ts.map +1 -0
- package/dist/commands/org.js +49 -0
- package/dist/commands/org.js.map +1 -0
- package/dist/commands/resources.d.ts +4 -0
- package/dist/commands/resources.d.ts.map +1 -0
- package/dist/commands/resources.js +22 -0
- package/dist/commands/resources.js.map +1 -0
- package/dist/commands/review.d.ts +8 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/review.js +74 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/skill.d.ts +19 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +313 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +45 -0
- package/dist/config.js.map +1 -0
- package/dist/generated/capabilities.json +770 -0
- package/dist/http/client.d.ts +12 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +91 -0
- package/dist/http/client.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/output/envelope.d.ts +45 -0
- package/dist/output/envelope.d.ts.map +1 -0
- package/dist/output/envelope.js +86 -0
- package/dist/output/envelope.js.map +1 -0
- package/dist/output/unwrap.d.ts +11 -0
- package/dist/output/unwrap.d.ts.map +1 -0
- package/dist/output/unwrap.js +33 -0
- package/dist/output/unwrap.js.map +1 -0
- package/dist/resolve.d.ts +24 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +24 -0
- package/dist/resolve.js.map +1 -0
- package/dist/skills/unzip.d.ts +14 -0
- package/dist/skills/unzip.d.ts.map +1 -0
- package/dist/skills/unzip.js +86 -0
- package/dist/skills/unzip.js.map +1 -0
- package/dist/update-check.d.ts +24 -0
- package/dist/update-check.d.ts.map +1 -0
- package/dist/update-check.js +204 -0
- package/dist/update-check.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +61 -0
- package/skills/soku/SKILL.md +225 -0
- package/skills/soku/references/capability-flow.md +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @soku-ai/cli
|
|
2
|
+
|
|
3
|
+
Call Soku ads/GA4 data capabilities from any shell or AI agent. It's the
|
|
4
|
+
preferred surface for external agents — equivalent to the hosted MCP server, but
|
|
5
|
+
works without an MCP host.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -g @soku-ai/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Development builds can be linked from this repo:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm --filter @soku-ai/cli run build
|
|
17
|
+
npm link apps/cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
soku auth login # device-login in the browser
|
|
24
|
+
soku org use <slug|id> # pick a workspace (id, slug, or name)
|
|
25
|
+
soku brand use <slug|id>
|
|
26
|
+
soku --help # discover namespaces (ads, ga4, …)
|
|
27
|
+
soku update-check # check npm for a newer CLI release
|
|
28
|
+
soku ads --help # actions in a namespace
|
|
29
|
+
soku ads list-ad-accounts --platform google
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Each data capability is a typed sub-command under its namespace; `<command>
|
|
33
|
+
--help` shows its flags. `soku call <ns> <action>` is a raw escape hatch for
|
|
34
|
+
actions not yet exposed as a typed sub-command.
|
|
35
|
+
|
|
36
|
+
## Authentication
|
|
37
|
+
|
|
38
|
+
`soku` uses an org-agnostic, device-login session token (RFC 8628). You log in
|
|
39
|
+
once; the org and brand are chosen at runtime and sent per request.
|
|
40
|
+
|
|
41
|
+
- **Human:** `soku auth login` opens the browser and waits for approval.
|
|
42
|
+
- **Agent (non-blocking):** `soku auth login --no-wait` returns the verification
|
|
43
|
+
URL immediately; resume with `soku auth login --device-code <code>` after the
|
|
44
|
+
user approves.
|
|
45
|
+
- **CI / headless:** set `SOKU_TOKEN` to a pre-issued token to skip interactive
|
|
46
|
+
auth (and the OS keychain).
|
|
47
|
+
|
|
48
|
+
Token storage: OS keychain when available, else `~/.soku/credentials.json`
|
|
49
|
+
(0600). Set `SOKU_NO_KEYCHAIN=1` to always use the file. Behind a proxy, set
|
|
50
|
+
`ALL_PROXY`.
|
|
51
|
+
|
|
52
|
+
## Output
|
|
53
|
+
|
|
54
|
+
stdout is JSON (`{"ok":true,"data":...}` when piped; pretty when a TTY).
|
|
55
|
+
Errors go to stderr as `{"ok":false,"error":{type,message,hint}}` with a
|
|
56
|
+
semantic exit code (0 ok / 1 usage / 2 auth / 4 not-found / 5 runtime).
|
|
57
|
+
|
|
58
|
+
## Use from an AI agent
|
|
59
|
+
|
|
60
|
+
For a fresh agent, point it at the hosted installer guide:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
Read https://soku.ai/cli/skill.md and install the Soku CLI.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Install the bundled skill so Claude Code / Codex / Cursor know how to drive the
|
|
67
|
+
CLI:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
soku skill install --global # all detected agents
|
|
71
|
+
soku skill install --agent claude # one agent, into the project
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Updates
|
|
75
|
+
|
|
76
|
+
`soku update-check` queries the npm registry for `@soku-ai/cli@latest` and tells
|
|
77
|
+
the user whether `npm i -g @soku-ai/cli` should be re-run. The CLI also performs a
|
|
78
|
+
TTY-only advisory check at most once every 24 hours, cached in
|
|
79
|
+
`~/.soku/update-check.json`. Set `SOKU_NO_UPDATE_CHECK=1` to disable the
|
|
80
|
+
background notice.
|
|
81
|
+
|
|
82
|
+
## Environment variables
|
|
83
|
+
|
|
84
|
+
| Variable | Purpose |
|
|
85
|
+
|----------|---------|
|
|
86
|
+
| `SOKU_TOKEN` | Pre-issued session token (overrides stored credentials) |
|
|
87
|
+
| `SOKU_API_BASE` | API base URL override |
|
|
88
|
+
| `SOKU_ORG_ID` / `SOKU_BRAND_ID` | One-off workspace override |
|
|
89
|
+
| `SOKU_NO_KEYCHAIN` | Skip the OS keychain; use the 0600 file |
|
|
90
|
+
| `SOKU_NO_UPDATE_CHECK` | Disable the TTY-only update notice |
|
|
91
|
+
| `ALL_PROXY` | Proxy for outbound HTTPS |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** RFC 8628 device authorization client. */
|
|
2
|
+
export interface DeviceAuthorization {
|
|
3
|
+
device_code: string;
|
|
4
|
+
user_code: string;
|
|
5
|
+
verification_uri: string;
|
|
6
|
+
verification_uri_complete: string;
|
|
7
|
+
expires_in: number;
|
|
8
|
+
interval: number;
|
|
9
|
+
}
|
|
10
|
+
export interface TokenResult {
|
|
11
|
+
access_token: string;
|
|
12
|
+
token_type: string;
|
|
13
|
+
expires_in: number;
|
|
14
|
+
scope: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function requestDeviceCode(opts: {
|
|
17
|
+
apiBase?: string;
|
|
18
|
+
scope?: string;
|
|
19
|
+
}): Promise<DeviceAuthorization>;
|
|
20
|
+
export type PollOutcome = {
|
|
21
|
+
status: 'token';
|
|
22
|
+
token: TokenResult;
|
|
23
|
+
} | {
|
|
24
|
+
status: 'denied';
|
|
25
|
+
} | {
|
|
26
|
+
status: 'expired';
|
|
27
|
+
};
|
|
28
|
+
/** Poll until the grant is approved/denied/expired, honoring slow_down (+5s). */
|
|
29
|
+
export declare function pollForToken(opts: {
|
|
30
|
+
deviceCode: string;
|
|
31
|
+
interval: number;
|
|
32
|
+
expiresIn: number;
|
|
33
|
+
apiBase?: string;
|
|
34
|
+
onTick?: () => void;
|
|
35
|
+
}): Promise<PollOutcome>;
|
|
36
|
+
//# sourceMappingURL=device.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../src/auth/device.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAI5C,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,EAAE,MAAM,CAAA;IACxB,yBAAyB,EAAE,MAAM,CAAA;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;CACd;AAID,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAW/B;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GACvC;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,GACpB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAA;AAEzB,iFAAiF;AACjF,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CACpB,GAAG,OAAO,CAAC,WAAW,CAAC,CA6CvB"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** RFC 8628 device authorization client. */
|
|
2
|
+
import { resolveApiBaseUrl } from '../config.js';
|
|
3
|
+
const CLIENT_ID = 'soku-cli';
|
|
4
|
+
export async function requestDeviceCode(opts) {
|
|
5
|
+
const base = resolveApiBaseUrl(opts.apiBase);
|
|
6
|
+
const res = await fetch(`${base}/api/device/code`, {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
body: JSON.stringify({ client_id: CLIENT_ID, scope: opts.scope ?? 'data-infra' }),
|
|
10
|
+
});
|
|
11
|
+
if (!res.ok) {
|
|
12
|
+
throw new Error(`Failed to start device authorization (HTTP ${res.status})`);
|
|
13
|
+
}
|
|
14
|
+
return (await res.json());
|
|
15
|
+
}
|
|
16
|
+
/** Poll until the grant is approved/denied/expired, honoring slow_down (+5s). */
|
|
17
|
+
export async function pollForToken(opts) {
|
|
18
|
+
const base = resolveApiBaseUrl(opts.apiBase);
|
|
19
|
+
let interval = Math.max(opts.interval, 1);
|
|
20
|
+
const deadline = Date.now() + opts.expiresIn * 1000;
|
|
21
|
+
while (Date.now() < deadline) {
|
|
22
|
+
await sleep(interval * 1000);
|
|
23
|
+
opts.onTick?.();
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await fetch(`${base}/api/device/token`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
31
|
+
device_code: opts.deviceCode,
|
|
32
|
+
client_id: CLIENT_ID,
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Transient network blip — keep polling until the deadline rather than
|
|
38
|
+
// giving up. RFC 8628 only terminates on a definitive error code.
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (res.ok) {
|
|
42
|
+
return { status: 'token', token: (await res.json()) };
|
|
43
|
+
}
|
|
44
|
+
const body = (await res.json().catch(() => ({})));
|
|
45
|
+
switch (body.error) {
|
|
46
|
+
case 'authorization_pending':
|
|
47
|
+
break;
|
|
48
|
+
case 'slow_down':
|
|
49
|
+
interval += 5;
|
|
50
|
+
break;
|
|
51
|
+
case 'access_denied':
|
|
52
|
+
return { status: 'denied' };
|
|
53
|
+
case 'expired_token':
|
|
54
|
+
return { status: 'expired' };
|
|
55
|
+
default:
|
|
56
|
+
// Unknown/transient server error (e.g. 5xx, 429): don't treat as a
|
|
57
|
+
// terminal expiry — keep polling until the deadline.
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { status: 'expired' };
|
|
62
|
+
}
|
|
63
|
+
function sleep(ms) {
|
|
64
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=device.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.js","sourceRoot":"","sources":["../../src/auth/device.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Token storage: env override → OS keychain (keytar) → 0600 file fallback.
|
|
2
|
+
*
|
|
3
|
+
* keytar is a native, optional dependency. In headless/CI/container environments
|
|
4
|
+
* (where agents typically run) it can fail to load entirely, so every keytar
|
|
5
|
+
* call is guarded and we fall back to a 0600 file. Agents/CI should prefer the
|
|
6
|
+
* SOKU_TOKEN env var, which bypasses storage completely.
|
|
7
|
+
*/
|
|
8
|
+
export declare function saveToken(token: string): Promise<void>;
|
|
9
|
+
export declare function loadToken(): Promise<string | null>;
|
|
10
|
+
export declare function clearToken(): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/auth/store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwCH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5D;AAED,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmBxD;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAchD"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Token storage: env override → OS keychain (keytar) → 0600 file fallback.
|
|
2
|
+
*
|
|
3
|
+
* keytar is a native, optional dependency. In headless/CI/container environments
|
|
4
|
+
* (where agents typically run) it can fail to load entirely, so every keytar
|
|
5
|
+
* call is guarded and we fall back to a 0600 file. Agents/CI should prefer the
|
|
6
|
+
* SOKU_TOKEN env var, which bypasses storage completely.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { configDir } from '../config.js';
|
|
11
|
+
const SERVICE = 'soku-cli';
|
|
12
|
+
const ACCOUNT = 'session';
|
|
13
|
+
let keytarWarned = false;
|
|
14
|
+
async function loadKeytar() {
|
|
15
|
+
// Explicit opt-out (CI/tests/containers): skip the OS keychain entirely.
|
|
16
|
+
if (process.env.SOKU_NO_KEYCHAIN)
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const mod = (await import('keytar'));
|
|
20
|
+
return mod.default ?? mod;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
if (!keytarWarned) {
|
|
24
|
+
process.stderr.write('soku: OS keychain unavailable; storing token in ~/.soku/credentials.json (0600). ' +
|
|
25
|
+
'Set SOKU_TOKEN to avoid on-disk storage.\n');
|
|
26
|
+
keytarWarned = true;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function credentialsPath() {
|
|
32
|
+
return join(configDir(), 'credentials.json');
|
|
33
|
+
}
|
|
34
|
+
export async function saveToken(token) {
|
|
35
|
+
const keytar = await loadKeytar();
|
|
36
|
+
if (keytar) {
|
|
37
|
+
try {
|
|
38
|
+
await keytar.setPassword(SERVICE, ACCOUNT, token);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// fall through to file
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const path = credentialsPath();
|
|
46
|
+
mkdirSync(configDir(), { recursive: true });
|
|
47
|
+
writeFileSync(path, JSON.stringify({ token }), { mode: 0o600 });
|
|
48
|
+
}
|
|
49
|
+
export async function loadToken() {
|
|
50
|
+
const fromEnv = process.env.SOKU_TOKEN?.trim();
|
|
51
|
+
if (fromEnv)
|
|
52
|
+
return fromEnv;
|
|
53
|
+
const keytar = await loadKeytar();
|
|
54
|
+
if (keytar) {
|
|
55
|
+
try {
|
|
56
|
+
const token = await keytar.getPassword(SERVICE, ACCOUNT);
|
|
57
|
+
if (token)
|
|
58
|
+
return token;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// fall through to file
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(readFileSync(credentialsPath(), 'utf8'));
|
|
66
|
+
return parsed.token ?? null;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function clearToken() {
|
|
73
|
+
const keytar = await loadKeytar();
|
|
74
|
+
if (keytar) {
|
|
75
|
+
try {
|
|
76
|
+
await keytar.deletePassword(SERVICE, ACCOUNT);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
rmSync(credentialsPath(), { force: true });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// ignore
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/auth/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAE1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAUnC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA4F3D"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** `soku auth login | status | logout` */
|
|
2
|
+
import open from 'open';
|
|
3
|
+
import qrcode from 'qrcode-terminal';
|
|
4
|
+
import { pollForToken, requestDeviceCode } from '../auth/device.js';
|
|
5
|
+
import { clearToken, loadToken, saveToken } from '../auth/store.js';
|
|
6
|
+
import { updateConfig } from '../config.js';
|
|
7
|
+
import { apiRequest } from '../http/client.js';
|
|
8
|
+
import { cyan, dim, emitError, emitSuccess, ExitCode, green } from '../output/envelope.js';
|
|
9
|
+
export function registerAuthCommands(program) {
|
|
10
|
+
const auth = program.command('auth').description('Authenticate the CLI with Soku');
|
|
11
|
+
auth
|
|
12
|
+
.command('login')
|
|
13
|
+
.description('Sign in via device authorization')
|
|
14
|
+
.option('--api-base <url>', 'Override the API base URL')
|
|
15
|
+
.option('--resource <bundles>', 'Resource bundles to request (comma-separated): data-infra, conversion-groups-write', 'data-infra')
|
|
16
|
+
.option('--no-wait', 'Print the verification URL and exit without polling (for agents)')
|
|
17
|
+
.option('--device-code <code>', 'Resume polling for a previously started login')
|
|
18
|
+
.option('--qr', 'Render the verification URL as a QR code')
|
|
19
|
+
.action(async (opts) => {
|
|
20
|
+
// Persist a non-default API base so subsequent commands reuse it.
|
|
21
|
+
if (opts.apiBase)
|
|
22
|
+
updateConfig({ apiBaseUrl: opts.apiBase });
|
|
23
|
+
// Split-flow resume: an agent started with --no-wait, the human approved,
|
|
24
|
+
// now poll for the token.
|
|
25
|
+
if (opts.deviceCode) {
|
|
26
|
+
await pollAndStore({
|
|
27
|
+
deviceCode: opts.deviceCode,
|
|
28
|
+
interval: 5,
|
|
29
|
+
expiresIn: 900,
|
|
30
|
+
apiBase: opts.apiBase,
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let auth;
|
|
35
|
+
try {
|
|
36
|
+
auth = await requestDeviceCode({ apiBase: opts.apiBase, scope: opts.resource });
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
emitError('device_code_failed', err.message, ExitCode.RUNTIME);
|
|
40
|
+
}
|
|
41
|
+
if (!opts.wait) {
|
|
42
|
+
// Non-blocking: hand the URL back to the caller (agent surfaces it to the
|
|
43
|
+
// human), then exit. The agent resumes with `--device-code` next turn.
|
|
44
|
+
emitSuccess({
|
|
45
|
+
device_code: auth.device_code,
|
|
46
|
+
user_code: auth.user_code,
|
|
47
|
+
verification_uri: auth.verification_uri,
|
|
48
|
+
verification_uri_complete: auth.verification_uri_complete,
|
|
49
|
+
expires_in: auth.expires_in,
|
|
50
|
+
interval: auth.interval,
|
|
51
|
+
next: `soku auth login --device-code ${auth.device_code}`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// Interactive (human) path.
|
|
55
|
+
process.stderr.write(`\n To connect, visit:\n ${auth.verification_uri}\n and enter code:\n ${auth.user_code}\n\n`);
|
|
56
|
+
if (opts.qr) {
|
|
57
|
+
qrcode.generate(auth.verification_uri_complete, { small: true });
|
|
58
|
+
}
|
|
59
|
+
await open(auth.verification_uri_complete).catch(() => undefined);
|
|
60
|
+
process.stderr.write(' Waiting for approval...\n');
|
|
61
|
+
await pollAndStore({
|
|
62
|
+
deviceCode: auth.device_code,
|
|
63
|
+
interval: auth.interval,
|
|
64
|
+
expiresIn: auth.expires_in,
|
|
65
|
+
apiBase: opts.apiBase,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
auth
|
|
69
|
+
.command('status')
|
|
70
|
+
.description('Show the current session')
|
|
71
|
+
.action(async () => {
|
|
72
|
+
const token = await loadToken();
|
|
73
|
+
if (!token) {
|
|
74
|
+
emitError('not_authenticated', 'Not signed in.', ExitCode.AUTH, 'Run `soku auth login`.');
|
|
75
|
+
}
|
|
76
|
+
const me = await apiRequest('/api/cli/me');
|
|
77
|
+
emitSuccess({ signed_in: true, ...me }, (d) => `${green('✓')} Signed in ${dim(`(${d.scope_type})`)}\n owner: ${cyan(d.owner_id)}`);
|
|
78
|
+
});
|
|
79
|
+
auth
|
|
80
|
+
.command('logout')
|
|
81
|
+
.description('Remove the stored session token')
|
|
82
|
+
.action(async () => {
|
|
83
|
+
await clearToken();
|
|
84
|
+
emitSuccess({ signed_out: true }, () => `${green('✓')} Signed out`);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async function pollAndStore(opts) {
|
|
88
|
+
const outcome = await pollForToken(opts);
|
|
89
|
+
if (outcome.status === 'denied') {
|
|
90
|
+
emitError('access_denied', 'Authorization was denied.', ExitCode.AUTH);
|
|
91
|
+
}
|
|
92
|
+
if (outcome.status === 'expired') {
|
|
93
|
+
emitError('expired', 'The login request expired before approval.', ExitCode.AUTH, 'Run `soku auth login` again.');
|
|
94
|
+
}
|
|
95
|
+
await saveToken(outcome.token.access_token);
|
|
96
|
+
const days = Math.round(outcome.token.expires_in / 86400);
|
|
97
|
+
emitSuccess({ signed_in: true, expires_in: outcome.token.expires_in, scope: outcome.token.scope }, () => `${green('✓')} Signed in ${dim(`(token valid ~${days} days)`)}\n ${dim('Next: soku org list')}`);
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAG1C,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,MAAM,MAAM,iBAAiB,CAAA;AAEpC,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAA4B,MAAM,mBAAmB,CAAA;AAC7F,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAA;AAE1F,MAAM,UAAU,oBAAoB,CAAC,OAAgB;IACnD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,gCAAgC,CAAC,CAAA;IAElF,IAAI;SACD,OAAO,CAAC,OAAO,CAAC;SAChB,WAAW,CAAC,kCAAkC,CAAC;SAC/C,MAAM,CAAC,kBAAkB,EAAE,2BAA2B,CAAC;SACvD,MAAM,CACL,sBAAsB,EACtB,oFAAoF,EACpF,YAAY,CACb;SACA,MAAM,CAAC,WAAW,EAAE,kEAAkE,CAAC;SACvF,MAAM,CAAC,sBAAsB,EAAE,+CAA+C,CAAC;SAC/E,MAAM,CAAC,MAAM,EAAE,0CAA0C,CAAC;SAC1D,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QACrB,kEAAkE;QAClE,IAAI,IAAI,CAAC,OAAO;YAAE,YAAY,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;QAE5D,0EAA0E;QAC1E,0BAA0B;QAC1B,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,MAAM,YAAY,CAAC;gBACjB,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,QAAQ,EAAE,CAAC;gBACX,SAAS,EAAE,GAAG;gBACd,OAAO,EAAE,IAAI,CAAC,OAAO;aACtB,CAAC,CAAA;YACF,OAAM;QACR,CAAC;QAED,IAAI,IAAyB,CAAA;QAC7B,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAA;QACjF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,SAAS,CAAC,oBAAoB,EAAG,GAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;QAC3E,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACf,0EAA0E;YAC1E,uEAAuE;YACvE,WAAW,CAAC;gBACV,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;gBACvC,yBAAyB,EAAE,IAAI,CAAC,yBAAyB;gBACzD,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,IAAI,EAAE,iCAAiC,IAAI,CAAC,WAAW,EAAE;aAC1D,CAAC,CAAA;QACJ,CAAC;QAED,4BAA4B;QAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,+BAA+B,IAAI,CAAC,gBAAgB,4BAA4B,IAAI,CAAC,SAAS,MAAM,CACrG,CAAA;QACD,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,yBAAyB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAClE,CAAC;QACD,MAAM,IAAI,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QACjE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAA;QAEnD,MAAM,YAAY,CAAC;YACjB,UAAU,EAAE,IAAI,CAAC,WAAW;YAC5B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,SAAS,EAAE,IAAI,CAAC,UAAU;YAC1B,OAAO,EAAE,IAAI,CAAC,OAAO;SACtB,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEJ,IAAI;SACD,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,0BAA0B,CAAC;SACvC,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,KAAK,GAAG,MAAM,SAAS,EAAE,CAAA;QAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,SAAS,CAAC,mBAAmB,EAAE,gBAAgB,EAAE,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC,CAAA;QAC3F,CAAC;QACD,MAAM,EAAE,GAAG,MAAM,UAAU,CAA2C,aAAa,CAAC,CAAA;QACpF,WAAW,CACT,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAC1B,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,cAAc,GAAG,CAAC,IAAI,CAAC,CAAC,UAAU,GAAG,CAAC,cAAc,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAC3F,CAAA;IACH,CAAC,CAAC,CAAA;IAEJ,IAAI;SACD,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,iCAAiC,CAAC;SAC9C,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,UAAU,EAAE,CAAA;QAClB,WAAW,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;IACrE,CAAC,CAAC,CAAA;AACN,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,IAK3B;IACC,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAA;IACxC,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAChC,SAAS,CAAC,eAAe,EAAE,2BAA2B,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;IACxE,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,SAAS,CACP,SAAS,EACT,4CAA4C,EAC5C,QAAQ,CAAC,IAAI,EACb,8BAA8B,CAC/B,CAAA;IACH,CAAC;IACD,MAAM,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAA;IAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,CAAA;IACzD,WAAW,CACT,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,EACrF,GAAG,EAAE,CACH,GAAG,KAAK,CAAC,GAAG,CAAC,cAAc,GAAG,CAAC,iBAAiB,IAAI,QAAQ,CAAC,OAAO,GAAG,CAAC,qBAAqB,CAAC,EAAE,CACnG,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand.d.ts","sourceRoot":"","sources":["../../src/commands/brand.ts"],"names":[],"mappings":"AAAA,mCAAmC;AAEnC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAcnC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAiF5D"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** `soku brand list | use <id>` */
|
|
2
|
+
import { loadConfig, updateConfig } from '../config.js';
|
|
3
|
+
import { apiRequest } from '../http/client.js';
|
|
4
|
+
import { cyan, dim, emitError, emitSuccess, ExitCode, green, table } from '../output/envelope.js';
|
|
5
|
+
import { matchRef } from '../resolve.js';
|
|
6
|
+
export function registerBrandCommands(program) {
|
|
7
|
+
const brand = program.command('brand').description('Manage the active brand');
|
|
8
|
+
brand
|
|
9
|
+
.command('list')
|
|
10
|
+
.description('List brands in the active organization')
|
|
11
|
+
.option('--org <orgId>', 'Organization to list brands for (defaults to active org)')
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
const orgId = opts.org || loadConfig().activeOrgId;
|
|
14
|
+
if (!orgId) {
|
|
15
|
+
emitError('no_org', 'No active organization.', ExitCode.USAGE, 'Run `soku org use <id>` or pass --org.');
|
|
16
|
+
}
|
|
17
|
+
const data = await apiRequest(`/api/cli/brands?org_id=${encodeURIComponent(orgId)}`);
|
|
18
|
+
const activeBrandId = loadConfig().activeBrandId;
|
|
19
|
+
emitSuccess(data, (d) => table(d.brands.map((b) => ({
|
|
20
|
+
active: b.id === activeBrandId ? green('●') : ' ',
|
|
21
|
+
name: b.name,
|
|
22
|
+
slug: b.slug,
|
|
23
|
+
id: b.id,
|
|
24
|
+
})), [
|
|
25
|
+
{ key: 'active', header: ' ' },
|
|
26
|
+
{ key: 'name', header: 'NAME' },
|
|
27
|
+
{ key: 'slug', header: 'SLUG' },
|
|
28
|
+
{ key: 'id', header: 'ID' },
|
|
29
|
+
]));
|
|
30
|
+
});
|
|
31
|
+
brand
|
|
32
|
+
.command('use <brand>')
|
|
33
|
+
.description('Set the active brand (accepts id, slug, or name)')
|
|
34
|
+
.action(async (ref) => {
|
|
35
|
+
const cfg = loadConfig();
|
|
36
|
+
if (!cfg.activeOrgId) {
|
|
37
|
+
emitError('no_org', 'Select an organization first.', ExitCode.USAGE, 'Run `soku org use <slug|id>`.');
|
|
38
|
+
}
|
|
39
|
+
const { brands } = await apiRequest(`/api/cli/brands?org_id=${encodeURIComponent(cfg.activeOrgId)}`);
|
|
40
|
+
const res = matchRef(brands, ref);
|
|
41
|
+
if (res.kind === 'none') {
|
|
42
|
+
emitError('not_found', `No brand matching "${ref}" in the active org.`, ExitCode.NOT_FOUND, 'Run `soku brand list` to see available brands.');
|
|
43
|
+
}
|
|
44
|
+
if (res.kind === 'ambiguous') {
|
|
45
|
+
const candidates = res.matches.map((b) => `${b.slug} (${b.id})`).join(', ');
|
|
46
|
+
emitError('ambiguous', `"${ref}" matches ${res.matches.length} brands.`, ExitCode.USAGE, `Use a slug or id: ${candidates}`);
|
|
47
|
+
}
|
|
48
|
+
const match = res.item;
|
|
49
|
+
updateConfig({ activeBrandId: match.id });
|
|
50
|
+
emitSuccess({ active_org_id: cfg.activeOrgId, active_brand_id: match.id, name: match.name }, (d) => `${green('✓')} Active brand: ${cyan(d.name)} ${dim(`(${d.active_brand_id})`)}\n ${dim('Next: soku --help (data commands under each namespace)')}`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=brand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand.js","sourceRoot":"","sources":["../../src/commands/brand.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"call.d.ts","sourceRoot":"","sources":["../../src/commands/call.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAMnC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyC1D"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** `soku call <namespace> <action> [--payload '<json>' | -p key=value ...]` */
|
|
2
|
+
import { apiRequest } from '../http/client.js';
|
|
3
|
+
import { emitError, emitSuccess, ExitCode } from '../output/envelope.js';
|
|
4
|
+
import { unwrapDispatch } from '../output/unwrap.js';
|
|
5
|
+
export function registerCallCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('call <namespace> <action>')
|
|
8
|
+
.description('Invoke a data capability')
|
|
9
|
+
.option('--payload <json>', 'Full JSON payload')
|
|
10
|
+
.option('-p, --param <key=value>', 'Set one payload field (repeatable); values are parsed as JSON when possible', collectParam, {})
|
|
11
|
+
.option('--summary <text>', 'Human-readable summary; required for review-gated write actions (becomes the HITL approval description)')
|
|
12
|
+
.action(async (namespace, action, opts) => {
|
|
13
|
+
let payload = {};
|
|
14
|
+
if (opts.payload) {
|
|
15
|
+
try {
|
|
16
|
+
payload = JSON.parse(opts.payload);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
emitError('usage', '--payload must be valid JSON.', ExitCode.USAGE);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
payload = { ...payload, ...opts.param };
|
|
23
|
+
// Review-gated write actions return a pending review; the server reads
|
|
24
|
+
// `_summary` as the human-facing approval description.
|
|
25
|
+
if (opts.summary)
|
|
26
|
+
payload._summary = opts.summary;
|
|
27
|
+
const result = await apiRequest(`/api/cli/call/${encodeURIComponent(namespace)}/${encodeURIComponent(action)}`, { method: 'POST', body: payload, workspace: true });
|
|
28
|
+
emitSuccess(unwrapDispatch(result));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function collectParam(entry, acc) {
|
|
32
|
+
const eq = entry.indexOf('=');
|
|
33
|
+
if (eq === -1) {
|
|
34
|
+
emitError('usage', `--param must be key=value, got: ${entry}`, ExitCode.USAGE);
|
|
35
|
+
}
|
|
36
|
+
const key = entry.slice(0, eq);
|
|
37
|
+
const raw = entry.slice(eq + 1);
|
|
38
|
+
let value = raw;
|
|
39
|
+
try {
|
|
40
|
+
value = JSON.parse(raw);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
value = raw; // keep as string
|
|
44
|
+
}
|
|
45
|
+
acc[key] = value;
|
|
46
|
+
return acc;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=call.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"call.js","sourceRoot":"","sources":["../../src/commands/call.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAI/E,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAEpD,MAAM,UAAU,mBAAmB,CAAC,OAAgB;IAClD,OAAO;SACJ,OAAO,CAAC,2BAA2B,CAAC;SACpC,WAAW,CAAC,0BAA0B,CAAC;SACvC,MAAM,CAAC,kBAAkB,EAAE,mBAAmB,CAAC;SAC/C,MAAM,CACL,yBAAyB,EACzB,6EAA6E,EAC7E,YAAY,EACZ,EAA6B,CAC9B;SACA,MAAM,CACL,kBAAkB,EAClB,yGAAyG,CAC1G;SACA,MAAM,CACL,KAAK,EACH,SAAiB,EACjB,MAAc,EACd,IAA4E,EAC5E,EAAE;QACF,IAAI,OAAO,GAA4B,EAAE,CAAA;QACzC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC;gBACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAA4B,CAAA;YAC/D,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS,CAAC,OAAO,EAAE,+BAA+B,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAA;YACrE,CAAC;QACH,CAAC;QACD,OAAO,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAA;QACvC,uEAAuE;QACvE,uDAAuD;QACvD,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAA;QAEjD,MAAM,MAAM,GAAG,MAAM,UAAU,CAC7B,iBAAiB,kBAAkB,CAAC,SAAS,CAAC,IAAI,kBAAkB,CAAC,MAAM,CAAC,EAAE,EAC9E,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CACnD,CAAA;QACD,WAAW,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAA;IACrC,CAAC,CACF,CAAA;AACL,CAAC;AAED,SAAS,YAAY,CAAC,KAAa,EAAE,GAA4B;IAC/D,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAC7B,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;QACd,SAAS,CAAC,OAAO,EAAE,mCAAmC,KAAK,EAAE,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAA;IAChF,CAAC;IACD,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAC9B,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;IAC/B,IAAI,KAAK,GAAY,GAAG,CAAA;IACxB,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,KAAK,GAAG,GAAG,CAAA,CAAC,iBAAiB;IAC/B,CAAC;IACD,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;IAChB,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** `soku egress -- <curl…>` — proxy a third-party API call through Soku so the
|
|
2
|
+
* credential is injected server-side (no API key on this machine), and
|
|
3
|
+
* `soku egress providers` — list the covered hosts.
|
|
4
|
+
*
|
|
5
|
+
* The agent prefixes its existing skill `curl` with `soku egress --`; we parse
|
|
6
|
+
* the curl, strip any placeholder auth header, and forward the request to
|
|
7
|
+
* `/api/cli/egress`. The upstream response is streamed back to stdout verbatim,
|
|
8
|
+
* so the skill sees exactly what a direct call would return. Only Soku-level
|
|
9
|
+
* failures (auth, allowlist, billing) become a CLI error envelope.
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
export interface ParsedCurl {
|
|
13
|
+
method?: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
headers: Record<string, string>;
|
|
16
|
+
body?: Buffer;
|
|
17
|
+
}
|
|
18
|
+
/** Extract method / url / headers / body from a curl-style token list. Pure. */
|
|
19
|
+
export declare function parseCurl(tokens: string[]): ParsedCurl;
|
|
20
|
+
/** Drop empty / bare-scheme auth headers so the server injects the real key
|
|
21
|
+
* (an empty `Authorization: Bearer ` would otherwise be treated as BYO). */
|
|
22
|
+
export declare function stripPlaceholderAuth(headers: Record<string, string>): Record<string, string>;
|
|
23
|
+
export declare function registerEgressCommands(program: Command): void;
|
|
24
|
+
//# sourceMappingURL=egress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egress.d.ts","sourceRoot":"","sources":["../../src/commands/egress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAOnC,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAWD,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CA0DtD;AAED;4EAC4E;AAC5E,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO5F;AA4FD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkC7D"}
|