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.
Files changed (58) hide show
  1. package/README.md +317 -20
  2. package/SKILL.md +20 -2
  3. package/bin/outrank-mcp.js +3 -0
  4. package/package.json +21 -5
  5. package/src/adapters/cli/jsonInput.js +42 -0
  6. package/src/adapters/cli/parse.js +124 -0
  7. package/src/adapters/cli/run.js +139 -0
  8. package/src/adapters/mcp/constants.js +49 -0
  9. package/src/adapters/mcp/descriptions.js +87 -0
  10. package/src/adapters/mcp/help.js +34 -0
  11. package/src/adapters/mcp/input.js +195 -0
  12. package/src/adapters/mcp/instructions.js +139 -0
  13. package/src/adapters/mcp/logger.js +45 -0
  14. package/src/adapters/mcp/main.js +36 -0
  15. package/src/adapters/mcp/options.js +118 -0
  16. package/src/adapters/mcp/result.js +33 -0
  17. package/src/adapters/mcp/server.js +174 -0
  18. package/src/adapters/mcp/session.js +274 -0
  19. package/src/adapters/mcp/toolNotes.js +426 -0
  20. package/src/adapters/mcp/toolsets.js +106 -0
  21. package/src/commands/articles.js +285 -301
  22. package/src/commands/auth.js +204 -86
  23. package/src/commands/billing.js +102 -104
  24. package/src/commands/improvements.js +319 -211
  25. package/src/commands/index.js +22 -0
  26. package/src/commands/keywords.js +266 -249
  27. package/src/commands/marketplace/buyer.js +386 -0
  28. package/src/commands/marketplace/index.js +83 -0
  29. package/src/commands/marketplace/notifications.js +60 -0
  30. package/src/commands/marketplace/seller.js +595 -0
  31. package/src/commands/marketplace/shared.js +27 -0
  32. package/src/commands/products/backlinks.js +181 -0
  33. package/src/commands/products/core.js +149 -0
  34. package/src/commands/products/gsc.js +250 -0
  35. package/src/commands/products/index.js +174 -0
  36. package/src/commands/products/integrations.js +343 -0
  37. package/src/commands/products/linking.js +192 -0
  38. package/src/commands/products/shared.js +8 -0
  39. package/src/commands/registry.js +272 -0
  40. package/src/commands/subscription.js +20 -30
  41. package/src/commands/usage.js +17 -30
  42. package/src/core/config.js +243 -62
  43. package/src/core/constants.js +101 -0
  44. package/src/core/context.js +311 -0
  45. package/src/core/errors.js +39 -0
  46. package/src/core/execute.js +71 -0
  47. package/src/core/files.js +186 -0
  48. package/src/core/http.js +393 -0
  49. package/src/core/profileSelection.js +7 -0
  50. package/src/core/query.js +11 -0
  51. package/src/core/redact.js +27 -0
  52. package/src/core/schema.js +222 -0
  53. package/src/core/version.js +12 -0
  54. package/src/index.js +30 -58
  55. package/src/commands/marketplace.js +0 -740
  56. package/src/commands/products.js +0 -1185
  57. package/src/core/api.js +0 -82
  58. 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
+ };