outrank-cli 0.5.0 → 0.6.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 +317 -20
- package/SKILL.md +20 -2
- package/bin/outrank-mcp.js +3 -0
- package/package.json +21 -5
- package/src/adapters/cli/jsonInput.js +42 -0
- package/src/adapters/cli/parse.js +124 -0
- package/src/adapters/cli/run.js +139 -0
- package/src/adapters/mcp/constants.js +49 -0
- package/src/adapters/mcp/descriptions.js +87 -0
- package/src/adapters/mcp/help.js +34 -0
- package/src/adapters/mcp/input.js +195 -0
- package/src/adapters/mcp/instructions.js +139 -0
- package/src/adapters/mcp/logger.js +45 -0
- package/src/adapters/mcp/main.js +36 -0
- package/src/adapters/mcp/options.js +118 -0
- package/src/adapters/mcp/result.js +33 -0
- package/src/adapters/mcp/server.js +174 -0
- package/src/adapters/mcp/session.js +274 -0
- package/src/adapters/mcp/toolNotes.js +426 -0
- package/src/adapters/mcp/toolsets.js +106 -0
- package/src/commands/articles.js +285 -301
- package/src/commands/auth.js +204 -86
- package/src/commands/billing.js +102 -104
- package/src/commands/improvements.js +319 -211
- package/src/commands/index.js +22 -0
- package/src/commands/keywords.js +266 -249
- package/src/commands/marketplace/buyer.js +386 -0
- package/src/commands/marketplace/index.js +83 -0
- package/src/commands/marketplace/notifications.js +60 -0
- package/src/commands/marketplace/seller.js +595 -0
- package/src/commands/marketplace/shared.js +27 -0
- package/src/commands/products/backlinks.js +181 -0
- package/src/commands/products/core.js +149 -0
- package/src/commands/products/gsc.js +250 -0
- package/src/commands/products/index.js +174 -0
- package/src/commands/products/integrations.js +343 -0
- package/src/commands/products/linking.js +192 -0
- package/src/commands/products/shared.js +8 -0
- package/src/commands/registry.js +272 -0
- package/src/commands/subscription.js +20 -30
- package/src/commands/usage.js +17 -30
- package/src/core/config.js +243 -62
- package/src/core/constants.js +101 -0
- package/src/core/context.js +311 -0
- package/src/core/errors.js +39 -0
- package/src/core/execute.js +71 -0
- package/src/core/files.js +186 -0
- package/src/core/http.js +393 -0
- package/src/core/profileSelection.js +7 -0
- package/src/core/query.js +11 -0
- package/src/core/redact.js +27 -0
- package/src/core/schema.js +222 -0
- package/src/core/version.js +12 -0
- package/src/index.js +30 -58
- package/src/commands/marketplace.js +0 -740
- package/src/commands/products.js +0 -1185
- package/src/core/api.js +0 -82
- package/src/core/args.js +0 -144
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { loadConfig } from '../../core/config.js';
|
|
2
|
+
import { MCP_STARTUP_RESOLVE_TIMEOUT_MS, Surface } from '../../core/constants.js';
|
|
3
|
+
import { createCommandContext, resolveContext } from '../../core/context.js';
|
|
4
|
+
import { MCP_USAGE_GUIDANCE, cliError, toErrorEnvelope } from '../../core/errors.js';
|
|
5
|
+
import { UUID_PATTERN } from '../../core/schema.js';
|
|
6
|
+
import {
|
|
7
|
+
FallbackReason,
|
|
8
|
+
IdentityStatus,
|
|
9
|
+
MAX_IDENTITY_TEXT_CHARS,
|
|
10
|
+
MCP_IDENTITY_RETRY_INTERVAL_MS,
|
|
11
|
+
SelectionSource,
|
|
12
|
+
} from './constants.js';
|
|
13
|
+
import { selectTools } from './toolsets.js';
|
|
14
|
+
|
|
15
|
+
const WHOAMI_PATH = '/auth/whoami';
|
|
16
|
+
const ENTITLEMENTS_FIELD = 'entitlements';
|
|
17
|
+
const KEY_REJECTED_CODES = Object.freeze([
|
|
18
|
+
'AUTH_MISSING',
|
|
19
|
+
'AUTH_INVALID_FORMAT',
|
|
20
|
+
'AUTH_KEY_INVALID',
|
|
21
|
+
'AUTH_KEY_REVOKED',
|
|
22
|
+
'AUTH_KEY_EXPIRED',
|
|
23
|
+
]);
|
|
24
|
+
const PERMANENT_IDENTITY_ERROR_CODES = Object.freeze([
|
|
25
|
+
...KEY_REJECTED_CODES,
|
|
26
|
+
'ACCESS_DENIED',
|
|
27
|
+
'CLI_AUTH_REQUIRED',
|
|
28
|
+
'CLI_INVALID_USAGE',
|
|
29
|
+
]);
|
|
30
|
+
const CONFIGURATION_NOTE =
|
|
31
|
+
' This is a problem with the MCP server configuration: tell the human, who must fix it outside this ' +
|
|
32
|
+
'conversation. The server re-reads the outrank-cli config file on the next call; a change to its command line ' +
|
|
33
|
+
'or environment needs a restart.';
|
|
34
|
+
const FIRST_PRINTABLE_CODE = 0x20;
|
|
35
|
+
const LAST_PRINTABLE_CODE = 0x7e;
|
|
36
|
+
|
|
37
|
+
const configurationFailure = (error) => {
|
|
38
|
+
const envelope = toErrorEnvelope(error, { usageGuidance: MCP_USAGE_GUIDANCE });
|
|
39
|
+
return cliError(
|
|
40
|
+
envelope.error.code,
|
|
41
|
+
envelope.error.message,
|
|
42
|
+
`${envelope.agent_guidance}${CONFIGURATION_NOTE}`,
|
|
43
|
+
envelope.error.details,
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const isPrintableAscii = (character) => {
|
|
48
|
+
const code = character.codePointAt(0);
|
|
49
|
+
return code >= FIRST_PRINTABLE_CODE && code <= LAST_PRINTABLE_CODE;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const boundedText = (value) => {
|
|
53
|
+
if (typeof value !== 'string') return null;
|
|
54
|
+
const text = [...value].filter(isPrintableAscii).join('').trim().slice(0, MAX_IDENTITY_TEXT_CHARS);
|
|
55
|
+
return text === '' ? null : text;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const uuidOrNull = (value) => (typeof value === 'string' && UUID_PATTERN.test(value) ? value : null);
|
|
59
|
+
|
|
60
|
+
const withBudget = async (promise, budgetMs) => {
|
|
61
|
+
let timer;
|
|
62
|
+
const expired = new Promise((resolve) => {
|
|
63
|
+
timer = setTimeout(resolve, budgetMs);
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
await Promise.race([promise, expired]);
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const toolNamesOf = (selection) => selection.tools.map((spec) => spec.id).join('\n');
|
|
73
|
+
|
|
74
|
+
const isKeyRejection = (envelope) => envelope?.ok === false && KEY_REJECTED_CODES.includes(envelope.error?.code);
|
|
75
|
+
|
|
76
|
+
export const createSession = ({
|
|
77
|
+
registry,
|
|
78
|
+
options,
|
|
79
|
+
env = process.env,
|
|
80
|
+
readConfig = loadConfig,
|
|
81
|
+
timeouts = {},
|
|
82
|
+
now = Date.now,
|
|
83
|
+
onToolsChanged = () => {},
|
|
84
|
+
}) => {
|
|
85
|
+
const startupBudgetMs = timeouts.startupResolveMs ?? MCP_STARTUP_RESOLVE_TIMEOUT_MS;
|
|
86
|
+
const retryIntervalMs = timeouts.identityRetryIntervalMs ?? MCP_IDENTITY_RETRY_INTERVAL_MS;
|
|
87
|
+
const activeRequests = new Set();
|
|
88
|
+
const state = {
|
|
89
|
+
resolved: null,
|
|
90
|
+
contextError: null,
|
|
91
|
+
identity: { status: IdentityStatus.IDLE },
|
|
92
|
+
identityEpoch: 0,
|
|
93
|
+
pending: null,
|
|
94
|
+
selection: null,
|
|
95
|
+
keyRejected: false,
|
|
96
|
+
lastRetryAt: null,
|
|
97
|
+
closed: false,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const failure = () => options.startupError ?? state.contextError;
|
|
101
|
+
|
|
102
|
+
const knownEntitlements = () =>
|
|
103
|
+
state.identity.status === IdentityStatus.OK && state.identity.hasEntitlements
|
|
104
|
+
? state.identity.entitlements
|
|
105
|
+
: undefined;
|
|
106
|
+
|
|
107
|
+
const refreshSelection = () => {
|
|
108
|
+
const previous = state.selection === null ? null : toolNamesOf(state.selection);
|
|
109
|
+
state.selection = selectTools({
|
|
110
|
+
specs: registry.commands,
|
|
111
|
+
entitlements: knownEntitlements(),
|
|
112
|
+
override: options.override,
|
|
113
|
+
readOnly: options.readOnly,
|
|
114
|
+
});
|
|
115
|
+
if (previous !== null && previous !== toolNamesOf(state.selection)) onToolsChanged();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const setIdentity = (identity) => {
|
|
119
|
+
state.identity = identity;
|
|
120
|
+
state.identityEpoch += 1;
|
|
121
|
+
refreshSelection();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const resolve = async () => {
|
|
125
|
+
const previousKey = state.resolved?.apiKey;
|
|
126
|
+
try {
|
|
127
|
+
state.resolved = resolveContext({
|
|
128
|
+
flags: { profile: options.profile },
|
|
129
|
+
env,
|
|
130
|
+
config: await readConfig(),
|
|
131
|
+
customBaseUrlFromStoredProfileOnly: true,
|
|
132
|
+
revealLocalDetails: false,
|
|
133
|
+
});
|
|
134
|
+
state.contextError = null;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
state.resolved = null;
|
|
137
|
+
state.contextError = configurationFailure(error);
|
|
138
|
+
}
|
|
139
|
+
state.keyRejected = false;
|
|
140
|
+
if (state.resolved?.apiKey !== previousKey) {
|
|
141
|
+
state.pending = null;
|
|
142
|
+
state.lastRetryAt = null;
|
|
143
|
+
setIdentity({ status: IdentityStatus.IDLE });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const openContext = ({ signal } = {}) => {
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
const forward = () => controller.abort(signal.reason);
|
|
150
|
+
if (state.closed) controller.abort();
|
|
151
|
+
else if (signal?.aborted) forward();
|
|
152
|
+
else if (signal) signal.addEventListener('abort', forward, { once: true });
|
|
153
|
+
activeRequests.add(controller);
|
|
154
|
+
const ctx = createCommandContext({
|
|
155
|
+
resolved: state.resolved,
|
|
156
|
+
registry,
|
|
157
|
+
surface: Surface.MCP,
|
|
158
|
+
signal: controller.signal,
|
|
159
|
+
cwd: options.uploadDir,
|
|
160
|
+
uploadDir: options.uploadDir,
|
|
161
|
+
usageGuidance: MCP_USAGE_GUIDANCE,
|
|
162
|
+
timeouts: {
|
|
163
|
+
...(timeouts.requestTimeoutMs !== undefined ? { requestTimeoutMs: timeouts.requestTimeoutMs } : {}),
|
|
164
|
+
...(timeouts.longRunningTimeoutMs !== undefined
|
|
165
|
+
? { longRunningTimeoutMs: timeouts.longRunningTimeoutMs }
|
|
166
|
+
: {}),
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
const release = () => {
|
|
170
|
+
activeRequests.delete(controller);
|
|
171
|
+
if (signal) signal.removeEventListener('abort', forward);
|
|
172
|
+
};
|
|
173
|
+
return { ctx, release };
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const applyWhoami = (envelope) => {
|
|
177
|
+
if (envelope?.ok !== true) {
|
|
178
|
+
if (isKeyRejection(envelope)) state.keyRejected = true;
|
|
179
|
+
setIdentity({ status: IdentityStatus.FAILED, error: envelope?.error ?? {} });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const data = envelope.data && typeof envelope.data === 'object' ? envelope.data : {};
|
|
183
|
+
setIdentity({
|
|
184
|
+
status: IdentityStatus.OK,
|
|
185
|
+
organizationId: uuidOrNull(data.organization?.id),
|
|
186
|
+
userEmail: boundedText(data.user?.email),
|
|
187
|
+
hasEntitlements: Object.hasOwn(data, ENTITLEMENTS_FIELD),
|
|
188
|
+
entitlements: data[ENTITLEMENTS_FIELD],
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const requestIdentity = () => {
|
|
193
|
+
if (state.pending) return state.pending;
|
|
194
|
+
const startedEpoch = state.identityEpoch;
|
|
195
|
+
const { ctx, release } = openContext();
|
|
196
|
+
const applyIfCurrent = (envelope) => {
|
|
197
|
+
if (state.identityEpoch === startedEpoch && !state.closed) applyWhoami(envelope);
|
|
198
|
+
};
|
|
199
|
+
const lookup = ctx
|
|
200
|
+
.http({ method: 'GET', path: WHOAMI_PATH })
|
|
201
|
+
.then(applyIfCurrent, (error) => applyIfCurrent(toErrorEnvelope(error)))
|
|
202
|
+
.finally(() => {
|
|
203
|
+
release();
|
|
204
|
+
if (state.pending === lookup) state.pending = null;
|
|
205
|
+
});
|
|
206
|
+
state.pending = lookup;
|
|
207
|
+
return lookup;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const canIdentify = () => failure() === null && Boolean(state.resolved?.apiKey);
|
|
211
|
+
|
|
212
|
+
const identityIsFinal = () => {
|
|
213
|
+
if (state.identity.status === IdentityStatus.OK) return true;
|
|
214
|
+
if (state.identity.status !== IdentityStatus.FAILED || state.pending) return false;
|
|
215
|
+
return PERMANENT_IDENTITY_ERROR_CODES.includes(state.identity.error?.code);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const needsResolve = () => state.resolved === null || !state.resolved.apiKey || state.keyRejected;
|
|
219
|
+
|
|
220
|
+
const retryIsDue = () => state.lastRetryAt === null || now() - state.lastRetryAt >= retryIntervalMs;
|
|
221
|
+
|
|
222
|
+
const start = async () => {
|
|
223
|
+
if (!options.startupError) await resolve();
|
|
224
|
+
refreshSelection();
|
|
225
|
+
if (canIdentify()) await withBudget(requestIdentity(), startupBudgetMs);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const ensureReady = async () => {
|
|
229
|
+
if (options.startupError) return;
|
|
230
|
+
if (needsResolve()) await resolve();
|
|
231
|
+
if (!canIdentify() || identityIsFinal()) return;
|
|
232
|
+
if (!state.pending) {
|
|
233
|
+
if (!retryIsDue()) return;
|
|
234
|
+
state.lastRetryAt = now();
|
|
235
|
+
}
|
|
236
|
+
await withBudget(requestIdentity(), startupBudgetMs);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const fallbackReason = () => {
|
|
240
|
+
if (state.selection?.source !== SelectionSource.FALLBACK) return null;
|
|
241
|
+
if (failure()) return FallbackReason.CONFIGURATION;
|
|
242
|
+
if (!state.resolved?.apiKey) return FallbackReason.NO_API_KEY;
|
|
243
|
+
if (state.identity.status === IdentityStatus.OK) return FallbackReason.NO_ENTITLEMENTS;
|
|
244
|
+
if (state.identity.status === IdentityStatus.FAILED) return FallbackReason.LOOKUP_FAILED;
|
|
245
|
+
return FallbackReason.LOOKUP_PENDING;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
start,
|
|
250
|
+
ensureReady,
|
|
251
|
+
openContext,
|
|
252
|
+
noteWhoami: (envelope) => {
|
|
253
|
+
if (envelope?.ok === true) applyWhoami(envelope);
|
|
254
|
+
},
|
|
255
|
+
noteResult: (envelope) => {
|
|
256
|
+
if (isKeyRejection(envelope)) state.keyRejected = true;
|
|
257
|
+
},
|
|
258
|
+
shutdown: () => {
|
|
259
|
+
state.closed = true;
|
|
260
|
+
for (const controller of activeRequests) controller.abort();
|
|
261
|
+
},
|
|
262
|
+
failure,
|
|
263
|
+
selection: () => state.selection,
|
|
264
|
+
identity: () => state.identity,
|
|
265
|
+
fallbackReason,
|
|
266
|
+
whenIdle: () => state.pending ?? Promise.resolve(),
|
|
267
|
+
organization: () => ({
|
|
268
|
+
id: state.identity.organizationId ?? null,
|
|
269
|
+
user_email: state.identity.userEmail ?? null,
|
|
270
|
+
profile: state.resolved?.profile.name ?? null,
|
|
271
|
+
base_url: state.resolved?.baseUrl ?? null,
|
|
272
|
+
}),
|
|
273
|
+
};
|
|
274
|
+
};
|