@typeship-ax/cli 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/LICENSE +9 -0
- package/README.md +40 -0
- package/api.json +5163 -0
- package/api.md +512 -0
- package/dist/cli-agent.d.ts +204 -0
- package/dist/cli-agent.d.ts.map +1 -0
- package/dist/cli-agent.js +525 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2811 -0
- package/dist/core/http.d.ts +303 -0
- package/dist/core/http.d.ts.map +1 -0
- package/dist/core/http.js +770 -0
- package/dist/core/pagination.d.ts +51 -0
- package/dist/core/pagination.d.ts.map +1 -0
- package/dist/core/pagination.js +154 -0
- package/dist/dates.d.ts +33 -0
- package/dist/dates.d.ts.map +1 -0
- package/dist/dates.js +136 -0
- package/dist/errors.d.ts +81 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +103 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/ops.d.ts +115 -0
- package/dist/ops.d.ts.map +1 -0
- package/dist/ops.js +79 -0
- package/dist/resources/account.d.ts +18 -0
- package/dist/resources/account.d.ts.map +1 -0
- package/dist/resources/account.js +26 -0
- package/dist/resources/api-keys.d.ts +37 -0
- package/dist/resources/api-keys.d.ts.map +1 -0
- package/dist/resources/api-keys.js +67 -0
- package/dist/resources/generate.d.ts +25 -0
- package/dist/resources/generate.d.ts.map +1 -0
- package/dist/resources/generate.js +41 -0
- package/dist/resources/generations.d.ts +31 -0
- package/dist/resources/generations.d.ts.map +1 -0
- package/dist/resources/generations.js +56 -0
- package/dist/resources/projects.d.ts +110 -0
- package/dist/resources/projects.d.ts.map +1 -0
- package/dist/resources/projects.js +220 -0
- package/dist/resources/spec-revisions.d.ts +47 -0
- package/dist/resources/spec-revisions.d.ts.map +1 -0
- package/dist/resources/spec-revisions.js +90 -0
- package/dist/schemas.d.ts +6 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +88 -0
- package/dist/types.d.ts +759 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +37 -0
- package/package.json +43 -0
- package/src/cli-agent.ts +685 -0
- package/src/cli.ts +2695 -0
- package/src/core/http.ts +1008 -0
- package/src/core/pagination.ts +195 -0
- package/src/dates.ts +126 -0
- package/src/errors.ts +117 -0
- package/src/index.ts +153 -0
- package/src/ops.ts +174 -0
- package/src/resources/account.ts +43 -0
- package/src/resources/api-keys.ts +105 -0
- package/src/resources/generate.ts +69 -0
- package/src/resources/generations.ts +100 -0
- package/src/resources/projects.ts +391 -0
- package/src/resources/spec-revisions.ts +150 -0
- package/src/schemas.ts +90 -0
- package/src/types.ts +825 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-agent.d.ts","sourceRoot":"","sources":["../src/cli-agent.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent contract of a generated CLI. Generated by typeship — https://typeship.dev
|
|
3
|
+
*
|
|
4
|
+
* Everything a coding agent needs from a command-line tool and a human does
|
|
5
|
+
* not: a JSON error envelope with stable codes and next steps, agent-mode
|
|
6
|
+
* detection, MCP client registration across the agents on a machine, an
|
|
7
|
+
* AGENTS.md block, `agent-guide` and `doctor`. Pure helpers over node:fs;
|
|
8
|
+
* cli.ts wires them to the commands. Zero dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
|
+
export function envelope(input) {
|
|
15
|
+
return {
|
|
16
|
+
status: input.status ?? "error",
|
|
17
|
+
issues: [{ code: input.code, message: input.message }],
|
|
18
|
+
...(input.docsUrl ? { docs_url: input.docsUrl } : {}),
|
|
19
|
+
next_steps: input.nextSteps ?? [],
|
|
20
|
+
...(input.detail !== undefined ? { detail: input.detail } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Exit code convention: 0 ok, 1 the request or command failed, 2 usage. */
|
|
24
|
+
export function exitCodeFor(code) {
|
|
25
|
+
switch (code) {
|
|
26
|
+
case "INVALID_USAGE":
|
|
27
|
+
case "UNKNOWN_COMMAND":
|
|
28
|
+
case "UNKNOWN_FLAG":
|
|
29
|
+
case "MISSING_ARGUMENT":
|
|
30
|
+
case "TTY_REQUIRED":
|
|
31
|
+
case "CONFIRMATION_REQUIRED":
|
|
32
|
+
return 2;
|
|
33
|
+
default:
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Interpret an SDK error result: HTTP status, body, transport, validation. */
|
|
38
|
+
export function classifyApiError(error, context) {
|
|
39
|
+
const e = (error ?? {});
|
|
40
|
+
// The message is the API's own words when it sent any, else the SDK's
|
|
41
|
+
// (the spec's response description). The error class name rides in
|
|
42
|
+
// detail, not in front of the message: "NotFoundError: No such account
|
|
43
|
+
// (no such account)" said one thing three times.
|
|
44
|
+
const base = e.message ? e.message : String(error);
|
|
45
|
+
if (e.violations !== undefined) {
|
|
46
|
+
return { code: "VALIDATION_FAILED", message: base, detail: { ...(e.name ? { error: e.name } : {}), violations: e.violations }, nextSteps: ["Fix the fields named in detail.violations, or drop --validate to send the body as is."] };
|
|
47
|
+
}
|
|
48
|
+
if (e.name === "TransportError" || (typeof e.status !== "number" && /fetch|ECONN|ENOTFOUND|timed out|TLS|abort/i.test(base))) {
|
|
49
|
+
return {
|
|
50
|
+
code: "NETWORK_ERROR",
|
|
51
|
+
message: base,
|
|
52
|
+
nextSteps: [
|
|
53
|
+
"Check the base URL (--base-url, the " + context.bin.toUpperCase().replace(/[^A-Z0-9]/g, "_") + "_BASE_URL variable, or '" + context.bin + " config base-url') and the network.",
|
|
54
|
+
"Retry once with backoff; do not loop.",
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const status = typeof e.status === "number" ? e.status : 0;
|
|
59
|
+
const body = e.body;
|
|
60
|
+
const apiCode = body?.errors?.[0]?.code;
|
|
61
|
+
const apiMessage = body?.errors?.[0]?.message ?? (typeof body?.message === "string" ? body.message : undefined) ?? (typeof body?.error === "string" ? body.error : undefined);
|
|
62
|
+
const detail = { status, ...(e.name ? { error: e.name } : {}), ...(e.body !== undefined ? { body: e.body } : {}) };
|
|
63
|
+
const same = apiMessage !== undefined && (apiMessage.toLowerCase() === base.toLowerCase() || base.toLowerCase().includes(apiMessage.toLowerCase()) || apiMessage.toLowerCase().includes(base.toLowerCase()));
|
|
64
|
+
const message = apiMessage === undefined ? base : same ? apiMessage : apiMessage + " (" + base + ")";
|
|
65
|
+
const upgradeUrl = extractUrl(e.body, ["upgrade_url", "upgradeUrl", "signup_url", "claim_url"]);
|
|
66
|
+
if (status === 401) {
|
|
67
|
+
return context.hadCredential
|
|
68
|
+
? { code: "AUTH_INVALID", message, detail, nextSteps: ["The credential was rejected. Check it is current: '" + context.bin + " auth check', then '" + context.bin + " login --help' to store a new one."] }
|
|
69
|
+
: { status: "action_required", code: "NO_AUTH", message, detail, nextSteps: ["No credential was sent. Set the auth env var, pass --token, or run '" + context.bin + " login'.", "'" + context.bin + " auth check' shows what the CLI would send."] };
|
|
70
|
+
}
|
|
71
|
+
if (status === 403)
|
|
72
|
+
return { code: "AUTH_INVALID", message, detail, nextSteps: ["The credential lacks access to this operation."] };
|
|
73
|
+
if (status === 402) {
|
|
74
|
+
return { status: "action_required", code: "PLAN_LIMIT", message, detail, nextSteps: [upgradeUrl ? "Lift the limit at " + upgradeUrl + ", then run the same command again." : "The account's plan stops here; upgrade it, then run the same command again.", "Do not retry the same call as is."] };
|
|
75
|
+
}
|
|
76
|
+
if (status === 404)
|
|
77
|
+
return { code: "NOT_FOUND", message, detail, nextSteps: notFoundNextSteps(message) };
|
|
78
|
+
if (status === 429) {
|
|
79
|
+
const retryAfter = extractRetryAfter(e);
|
|
80
|
+
return { status: "action_required", code: "RATE_LIMITED", message, detail, nextSteps: [retryAfter ? "Wait " + retryAfter + " seconds, then run the same command again." : "Back off and retry once; the SDK already retried with the server's Retry-After."] };
|
|
81
|
+
}
|
|
82
|
+
if (status === 422 && apiCode === "spec_error")
|
|
83
|
+
return { code: "SPEC_INVALID", message, detail, nextSteps: ["The API rejected the spec it was given; the message says why.", context.docsUrl ? "Look the message up: '" + context.bin + " docs search \"" + (apiMessage ?? "").slice(0, 60).replace(/"/g, "'") + "\"'." : "Fix the spec and run again."] };
|
|
84
|
+
if (status === 400 || status === 422 || status === 409 || status === 413)
|
|
85
|
+
return { code: "INVALID_REQUEST", message, detail, nextSteps: ["Read detail.body for the field the API named; run the command with --help for its flags."] };
|
|
86
|
+
if (status >= 500)
|
|
87
|
+
return { code: "SERVER_ERROR", message, detail, nextSteps: ["Retry once with backoff. If it persists, report the request id in detail.body."] };
|
|
88
|
+
return { code: "COMMAND_FAILED", message, detail };
|
|
89
|
+
}
|
|
90
|
+
/** A file lookup needs path guidance, not the generic advice for a missing
|
|
91
|
+
* resource id. Prefer the API's own index when its message names one. */
|
|
92
|
+
function notFoundNextSteps(message) {
|
|
93
|
+
if (/\bfiles_index\b/i.test(message)) {
|
|
94
|
+
return ["Read files_index on the generation, choose an exact path it lists, then run the command again with that path."];
|
|
95
|
+
}
|
|
96
|
+
if (/\bfile\b/i.test(message) && /\bpaths?\b/i.test(message)) {
|
|
97
|
+
return ["Check the requested file path against the API's file listing, then run the command again with an exact path."];
|
|
98
|
+
}
|
|
99
|
+
return ["Check the resource id; list the resource first."];
|
|
100
|
+
}
|
|
101
|
+
function extractUrl(body, keys) {
|
|
102
|
+
if (!body || typeof body !== "object")
|
|
103
|
+
return undefined;
|
|
104
|
+
const record = body;
|
|
105
|
+
for (const key of keys) {
|
|
106
|
+
const direct = record[key];
|
|
107
|
+
if (typeof direct === "string")
|
|
108
|
+
return direct;
|
|
109
|
+
for (const value of Object.values(record)) {
|
|
110
|
+
if (value && typeof value === "object" && typeof value[key] === "string")
|
|
111
|
+
return value[key];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
function extractRetryAfter(e) {
|
|
117
|
+
const headers = e.headers;
|
|
118
|
+
const fromHeader = headers?.get?.("retry-after");
|
|
119
|
+
if (fromHeader)
|
|
120
|
+
return fromHeader;
|
|
121
|
+
const match = /retry after (\d+)s/i.exec(JSON.stringify(e.body ?? ""));
|
|
122
|
+
return match?.[1];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* --mode agent beats <PREFIX>_MODE=agent beats "no terminal on either end".
|
|
126
|
+
* In agent mode nothing prompts, browsers are not opened, and every stop is
|
|
127
|
+
* an action_required envelope with next_steps.
|
|
128
|
+
*/
|
|
129
|
+
export function agentMode(input) {
|
|
130
|
+
if (input.flagMode === "agent")
|
|
131
|
+
return true;
|
|
132
|
+
if (input.flagMode === "human" || input.flagMode === "interactive")
|
|
133
|
+
return false;
|
|
134
|
+
if (input.envMode === "agent")
|
|
135
|
+
return true;
|
|
136
|
+
if (input.envMode === "human" || input.envMode === "interactive")
|
|
137
|
+
return false;
|
|
138
|
+
return !input.stdoutIsTTY && !input.stdinIsTTY;
|
|
139
|
+
}
|
|
140
|
+
/** Which agent harness is running us, from its environment; analytics and AGENTS.md placement only. */
|
|
141
|
+
export function detectHarness(env = process.env) {
|
|
142
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDE_CODE_ENTRYPOINT)
|
|
143
|
+
return "claude-code";
|
|
144
|
+
if (env.CURSOR_TRACE_ID || env.CURSOR_AGENT)
|
|
145
|
+
return "cursor";
|
|
146
|
+
if (env.CODEX_SANDBOX || env.CODEX_CI || env.OPENAI_CODEX)
|
|
147
|
+
return "codex";
|
|
148
|
+
if (env.GEMINI_CLI || env.GEMINI_API_KEY && env.GEMINI_SANDBOX)
|
|
149
|
+
return "gemini-cli";
|
|
150
|
+
if (env.WINDSURF_SESSION || env.CODEIUM_AGENT)
|
|
151
|
+
return "windsurf";
|
|
152
|
+
if (env.OPENCODE || env.OPENCODE_SESSION)
|
|
153
|
+
return "opencode";
|
|
154
|
+
if (env.AMP_SESSION || env.AMP_AGENT)
|
|
155
|
+
return "amp";
|
|
156
|
+
if (env.DEVIN_SESSION)
|
|
157
|
+
return "devin";
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function readOr(file, fallback) {
|
|
161
|
+
try {
|
|
162
|
+
return readFileSync(file, "utf8");
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return fallback;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function jsonMerge(existing, path, name, value) {
|
|
169
|
+
let doc = {};
|
|
170
|
+
try {
|
|
171
|
+
doc = JSON.parse(existing);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
doc = {};
|
|
175
|
+
}
|
|
176
|
+
let cursor = doc;
|
|
177
|
+
for (const key of path) {
|
|
178
|
+
const next = cursor[key];
|
|
179
|
+
if (!next || typeof next !== "object" || Array.isArray(next))
|
|
180
|
+
cursor[key] = {};
|
|
181
|
+
cursor = cursor[key];
|
|
182
|
+
}
|
|
183
|
+
cursor[name] = value;
|
|
184
|
+
return JSON.stringify(doc, null, 2) + "\n";
|
|
185
|
+
}
|
|
186
|
+
const httpEntry = (entry) => ({ type: "http", url: entry.url, ...(entry.headers ? { headers: entry.headers } : {}) });
|
|
187
|
+
const stdioEntry = (entry) => ({ command: entry.command, args: entry.args ?? [], ...(entry.env ? { env: entry.env } : {}) });
|
|
188
|
+
const standardEntry = (entry) => (entry.url ? httpEntry(entry) : stdioEntry(entry));
|
|
189
|
+
/** TOML for Codex's config.toml: append or replace the [mcp_servers.<name>] table. */
|
|
190
|
+
function tomlMerge(existing, name, entry) {
|
|
191
|
+
const header = "[mcp_servers." + name + "]";
|
|
192
|
+
const lines = [header];
|
|
193
|
+
if (entry.url) {
|
|
194
|
+
lines.push("url = " + JSON.stringify(entry.url));
|
|
195
|
+
const auth = entry.headers?.Authorization ?? entry.headers?.authorization;
|
|
196
|
+
const envRef = auth ? /\$\{([A-Z0-9_]+)\}/.exec(auth)?.[1] : undefined;
|
|
197
|
+
if (envRef)
|
|
198
|
+
lines.push("bearer_token_env_var = " + JSON.stringify(envRef));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
lines.push("command = " + JSON.stringify(entry.command ?? "node"));
|
|
202
|
+
lines.push("args = " + JSON.stringify(entry.args ?? []));
|
|
203
|
+
}
|
|
204
|
+
const block = lines.join("\n") + "\n";
|
|
205
|
+
const pattern = new RegExp("\\[mcp_servers\\." + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\][\\s\\S]*?(?=\\n\\[|$)");
|
|
206
|
+
if (pattern.test(existing))
|
|
207
|
+
return existing.replace(pattern, block.trimEnd() + "\n");
|
|
208
|
+
return (existing.trimEnd() + (existing.trim() ? "\n\n" : "")) + block;
|
|
209
|
+
}
|
|
210
|
+
const home = () => homedir();
|
|
211
|
+
const xdg = () => process.env.XDG_CONFIG_HOME ?? join(home(), ".config");
|
|
212
|
+
export const MCP_CLIENTS = [
|
|
213
|
+
{
|
|
214
|
+
id: "claude-code",
|
|
215
|
+
label: "Claude Code",
|
|
216
|
+
file: (cwd) => join(cwd, ".mcp.json"),
|
|
217
|
+
detect: () => existsSync(join(home(), ".claude")) || existsSync(join(home(), ".claude.json")),
|
|
218
|
+
write: (existing, name, entry) => jsonMerge(existing, ["mcpServers"], name, standardEntry(entry)),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "codex",
|
|
222
|
+
label: "Codex CLI",
|
|
223
|
+
file: () => join(home(), ".codex", "config.toml"),
|
|
224
|
+
detect: () => existsSync(join(home(), ".codex")),
|
|
225
|
+
write: (existing, name, entry) => tomlMerge(existing, name, entry),
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: "vscode",
|
|
229
|
+
label: "VS Code",
|
|
230
|
+
file: (cwd) => join(cwd, ".vscode", "mcp.json"),
|
|
231
|
+
detect: (cwd) => existsSync(join(cwd, ".vscode")) || existsSync(join(home(), ".vscode")),
|
|
232
|
+
write: (existing, name, entry) => jsonMerge(existing, ["servers"], name, standardEntry(entry)),
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: "windsurf",
|
|
236
|
+
label: "Windsurf",
|
|
237
|
+
file: () => join(home(), ".codeium", "windsurf", "mcp_config.json"),
|
|
238
|
+
detect: () => existsSync(join(home(), ".codeium", "windsurf")),
|
|
239
|
+
write: (existing, name, entry) => jsonMerge(existing, ["mcpServers"], name, entry.url ? { serverUrl: entry.url, ...(entry.headers ? { headers: entry.headers } : {}) } : stdioEntry(entry)),
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: "gemini-cli",
|
|
243
|
+
label: "Gemini CLI",
|
|
244
|
+
file: () => join(home(), ".gemini", "settings.json"),
|
|
245
|
+
detect: () => existsSync(join(home(), ".gemini")),
|
|
246
|
+
write: (existing, name, entry) => jsonMerge(existing, ["mcpServers"], name, entry.url ? { httpUrl: entry.url, ...(entry.headers ? { headers: entry.headers } : {}) } : stdioEntry(entry)),
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: "opencode",
|
|
250
|
+
label: "OpenCode",
|
|
251
|
+
file: () => join(xdg(), "opencode", "opencode.json"),
|
|
252
|
+
detect: () => existsSync(join(xdg(), "opencode")),
|
|
253
|
+
write: (existing, name, entry) => jsonMerge(existing, ["mcp"], name, entry.url ? { type: "remote", url: entry.url, ...(entry.headers ? { headers: entry.headers } : {}) } : { type: "local", command: [entry.command ?? "node", ...(entry.args ?? [])] }),
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "zed",
|
|
257
|
+
label: "Zed",
|
|
258
|
+
file: () => join(xdg(), "zed", "settings.json"),
|
|
259
|
+
detect: () => existsSync(join(xdg(), "zed")),
|
|
260
|
+
write: (existing, name, entry) => jsonMerge(existing, ["context_servers"], name, entry.url ? { source: "custom", url: entry.url, ...(entry.headers ? { headers: entry.headers } : {}) } : { source: "custom", command: entry.command ?? "node", args: entry.args ?? [] }),
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: "claude-desktop",
|
|
264
|
+
label: "Claude Desktop",
|
|
265
|
+
file: () => process.platform === "darwin"
|
|
266
|
+
? join(home(), "Library", "Application Support", "Claude", "claude_desktop_config.json")
|
|
267
|
+
: process.platform === "win32"
|
|
268
|
+
? join(process.env.APPDATA ?? join(home(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json")
|
|
269
|
+
: join(xdg(), "Claude", "claude_desktop_config.json"),
|
|
270
|
+
detect: () => existsSync(dirname(MCP_CLIENTS.find((c) => c.id === "claude-desktop").file(""))),
|
|
271
|
+
write: (existing, name, entry) => jsonMerge(existing, ["mcpServers"], name, stdioEntry(entry)),
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: "cursor",
|
|
275
|
+
label: "Cursor",
|
|
276
|
+
file: (cwd) => join(cwd, ".cursor", "mcp.json"),
|
|
277
|
+
detect: () => existsSync(join(home(), ".cursor")),
|
|
278
|
+
write: (existing, name, entry) => jsonMerge(existing, ["mcpServers"], name, standardEntry(entry)),
|
|
279
|
+
incompatible: "Cursor does not yet speak MCP 2026-07-28, which is the only version this server serves; the entry is written but Cursor will not connect until it does.",
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
export function findMcpClient(id) {
|
|
283
|
+
return MCP_CLIENTS.find((c) => c.id === id);
|
|
284
|
+
}
|
|
285
|
+
/** Merge the entry into one client's config file. Never writes a literal secret: callers pass env references. */
|
|
286
|
+
export function writeMcpConfig(client, cwd, name, entry) {
|
|
287
|
+
const file = client.file(cwd);
|
|
288
|
+
if (!entry.url && client.id === "claude-desktop" && !entry.command) {
|
|
289
|
+
return { client: client.id, file, written: false, note: "Claude Desktop reads only stdio servers from its config file; add a remote server as a connector in the app." };
|
|
290
|
+
}
|
|
291
|
+
const existing = readOr(file, "");
|
|
292
|
+
const next = client.write(existing, name, entry);
|
|
293
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
294
|
+
writeFileSync(file, next);
|
|
295
|
+
return { client: client.id, file, written: true, ...(client.incompatible ? { note: client.incompatible } : {}) };
|
|
296
|
+
}
|
|
297
|
+
/** Does a client's config already mention this server? For doctor. */
|
|
298
|
+
export function mcpConfigured(client, cwd, name) {
|
|
299
|
+
const text = readOr(client.file(cwd), "");
|
|
300
|
+
return text.includes('"' + name + '"') || text.includes("[mcp_servers." + name + "]");
|
|
301
|
+
}
|
|
302
|
+
// ---- AGENTS.md block ----------------------------------------------------------
|
|
303
|
+
/**
|
|
304
|
+
* Upsert a marked block into a repo's agent instructions. Idempotent: the
|
|
305
|
+
* block between the markers is replaced, everything else is untouched, and
|
|
306
|
+
* a missing file is created. Returns the file written and whether the
|
|
307
|
+
* block already existed.
|
|
308
|
+
*/
|
|
309
|
+
export function upsertAgentBlock(file, marker, body) {
|
|
310
|
+
const start = "<!-- " + marker + " start -->";
|
|
311
|
+
const end = "<!-- " + marker + " end -->";
|
|
312
|
+
const block = start + "\n" + body.trim() + "\n" + end + "\n";
|
|
313
|
+
const existing = readOr(file, "");
|
|
314
|
+
const pattern = new RegExp(escapeRegExp(start) + "[\\s\\S]*?" + escapeRegExp(end) + "\\n?");
|
|
315
|
+
let next;
|
|
316
|
+
let updated = false;
|
|
317
|
+
if (pattern.test(existing)) {
|
|
318
|
+
next = existing.replace(pattern, block);
|
|
319
|
+
updated = true;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
next = (existing.trimEnd() + (existing.trim() ? "\n\n" : "")) + block;
|
|
323
|
+
}
|
|
324
|
+
mkdirSync(dirname(resolve(file)), { recursive: true });
|
|
325
|
+
writeFileSync(file, next);
|
|
326
|
+
return { file, updated };
|
|
327
|
+
}
|
|
328
|
+
function escapeRegExp(text) {
|
|
329
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
330
|
+
}
|
|
331
|
+
/** AGENTS.md by default; CLAUDE.md when only it exists, or when running under Claude Code and AGENTS.md is absent. */
|
|
332
|
+
export function agentInstructionsFile(cwd, harness) {
|
|
333
|
+
const agents = join(cwd, "AGENTS.md");
|
|
334
|
+
const claude = join(cwd, "CLAUDE.md");
|
|
335
|
+
if (existsSync(agents))
|
|
336
|
+
return agents;
|
|
337
|
+
if (existsSync(claude))
|
|
338
|
+
return claude;
|
|
339
|
+
return harness === "claude-code" ? claude : agents;
|
|
340
|
+
}
|
|
341
|
+
/** One line per command, pipe-delimited: the compact index that goes into AGENTS.md. */
|
|
342
|
+
/** string, number, usd|eur, string[], usd|eur[], object, json — the type as a reader expects it. */
|
|
343
|
+
export function flagTypeLabel(f) {
|
|
344
|
+
const inline = (values) => values && values.join("|").length <= 24 ? values.join("|") : values ? "enum" : undefined;
|
|
345
|
+
if (f.type === "array")
|
|
346
|
+
return (inline(f.items?.enum) ?? f.items?.type ?? "json") + "[]";
|
|
347
|
+
return inline(f.enum) ?? f.type;
|
|
348
|
+
}
|
|
349
|
+
export function compactIndex(commands) {
|
|
350
|
+
return commands
|
|
351
|
+
.map((c) => {
|
|
352
|
+
const required = c.flags.filter((f) => f.required).map((f) => "--" + f.flag + (f.type === "boolean" ? "" : " <" + flagTypeLabel(f) + ">"));
|
|
353
|
+
return c.resource + " " + c.command + " | " + c.method + " " + c.path + (required.length ? " | " + required.join(" ") : "") + (c.summary ? " | " + c.summary : "") + (c.auth === "none" ? " | no auth" : "");
|
|
354
|
+
})
|
|
355
|
+
.join("\n");
|
|
356
|
+
}
|
|
357
|
+
/** The AGENTS.md block body. */
|
|
358
|
+
export function agentBlock(ctx, commands) {
|
|
359
|
+
const auth = ctx.authEnvVars.length ? ctx.authEnvVars.join(", ") : "(none)";
|
|
360
|
+
return [
|
|
361
|
+
"## " + ctx.bin + " CLI (" + ctx.apiTitle + ")",
|
|
362
|
+
"",
|
|
363
|
+
"Generated by typeship. API commands write JSON on stdout; discovery commands take --json. Errors are JSON on stderr ({status, issues[{code,message}], next_steps}), exit 0/1/2. Non-interactive under an agent: no prompts, no browsers.",
|
|
364
|
+
"",
|
|
365
|
+
"- Auth: " + auth + " in the environment, or `" + ctx.bin + " login`. Never write a key into a file in this repo.",
|
|
366
|
+
"- Discover: `" + ctx.bin + " --help`, `" + ctx.bin + " <resource> <command> --help`, `" + ctx.bin + " help --json` (machine-readable), `" + ctx.bin + " agent-guide --format json`.",
|
|
367
|
+
"- Docs: " + (ctx.docsUrl ? "`" + ctx.bin + " docs search <term> --json`; " + ctx.docsUrl + "/llms.txt" : "`" + ctx.bin + " docs <resource> <command> --json`") + ".",
|
|
368
|
+
"- Lists: `--all` streams every page as NDJSON. Destructive commands need `--force`.",
|
|
369
|
+
...(ctx.hasMcp ? ["- MCP: `" + ctx.bin + " mcp install --all` registers this API's MCP server with the agent clients on this machine" + (ctx.mcpUrl ? " (hosted: " + ctx.mcpUrl + ")" : "") + "."] : []),
|
|
370
|
+
"",
|
|
371
|
+
"Commands (resource command | METHOD path | required flags | summary):",
|
|
372
|
+
"",
|
|
373
|
+
"```",
|
|
374
|
+
compactIndex(commands),
|
|
375
|
+
"```",
|
|
376
|
+
].join("\n");
|
|
377
|
+
}
|
|
378
|
+
/** What `agent-guide --format json` returns. */
|
|
379
|
+
export function agentGuide(ctx, commands) {
|
|
380
|
+
const first = commands.find((c) => c.method === "GET" && c.flags.every((f) => !f.required)) ?? commands[0];
|
|
381
|
+
return {
|
|
382
|
+
name: ctx.bin,
|
|
383
|
+
package: ctx.pkg,
|
|
384
|
+
api: ctx.apiTitle,
|
|
385
|
+
version: ctx.version,
|
|
386
|
+
generated_by: "typeship",
|
|
387
|
+
guide: agentBlock(ctx, commands),
|
|
388
|
+
first_command: first ? ctx.bin + " " + first.resource + " " + first.command : ctx.bin + " --help",
|
|
389
|
+
docs_index_url: ctx.docsUrl ? ctx.docsUrl.replace(/\/+$/, "") + "/llms.txt" : null,
|
|
390
|
+
docs_full_url: ctx.docsUrl ? ctx.docsUrl.replace(/\/+$/, "") + "/llms-full.txt" : null,
|
|
391
|
+
hosted_mcp_url: ctx.mcpUrl,
|
|
392
|
+
local_mcp: ctx.hasMcp ? ctx.bin + "-mcp (stdio) or '" + ctx.bin + " mcp install --all'" : null,
|
|
393
|
+
skills_install: ctx.skillsRepo ? "npx skills add " + ctx.skillsRepo : null,
|
|
394
|
+
auth_env_vars: ctx.authEnvVars,
|
|
395
|
+
conventions: {
|
|
396
|
+
output: "API commands return JSON on stdout; discovery commands take --json. --fields a,b.c keeps only those paths (per item for lists); --all streams NDJSON for paginated lists; SSE operations stream NDJSON events.",
|
|
397
|
+
errors: "JSON envelope on stderr: {status: 'error'|'action_required', issues: [{code, message}], docs_url?, next_steps: [], detail?}. Branch on issues[].code.",
|
|
398
|
+
exit_codes: { "0": "ok", "1": "the request or command failed", "2": "usage: wrong flags, missing arguments, or a confirmation was required" },
|
|
399
|
+
agent_mode: "--mode agent, " + ctx.envPrefix + "_MODE=agent, or no terminal on stdin and stdout: no prompts, no browsers, --yes implied for non-destructive stops.",
|
|
400
|
+
destructive: "Commands classified as destructive need --force (or --yes); without it they return CONFIRMATION_REQUIRED with the exact command to run.",
|
|
401
|
+
flags: "Positional path arguments first, then --flags. Array flags take a comma list, the flag repeated, or a JSON array; object flags take JSON. --data '<json>' merges under field flags (@<file> reads a file, - reads stdin).",
|
|
402
|
+
pagination: "Paginated commands return {items, hasMore, nextPage, nextCommand}: run nextCommand for the next page, or --all walks every page.",
|
|
403
|
+
},
|
|
404
|
+
builtins: ctx.builtins,
|
|
405
|
+
next_steps: [
|
|
406
|
+
...(ctx.authEnvVars.length ? ["Set " + ctx.authEnvVars[0] + " in the environment or run '" + ctx.bin + " login'."] : []),
|
|
407
|
+
"Run '" + ctx.bin + " auth check'.",
|
|
408
|
+
"Run '" + ctx.bin + " help --json' for the command index, or read the AGENTS.md block '" + ctx.bin + " init' writes.",
|
|
409
|
+
...(ctx.hasMcp ? ["Run '" + ctx.bin + " mcp install --all' if this session has an MCP-capable client."] : []),
|
|
410
|
+
],
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/** Install a skills repository through the skills CLI (npx skills add). Best effort: init reports, never fails on it. */
|
|
414
|
+
export function installSkills(repo, options = {}) {
|
|
415
|
+
if (!repo)
|
|
416
|
+
return { status: "skipped", repo: null, detail: "No skills repository is configured for this CLI." };
|
|
417
|
+
const args = ["-y", "skills", "add", repo, "-y", ...(options.global === false ? [] : ["-g"])];
|
|
418
|
+
if (options.agent)
|
|
419
|
+
args.push("-a", options.agent);
|
|
420
|
+
// Windows resolves npx to npx.cmd, which only a shell can start.
|
|
421
|
+
const result = spawnSync("npx", args, { encoding: "utf8", timeout: 120_000, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
422
|
+
if (result.error || result.status !== 0) {
|
|
423
|
+
return { status: "failed", repo, detail: (result.stderr || result.stdout || result.error?.message || "npx skills add failed").toString().trim().slice(-400) };
|
|
424
|
+
}
|
|
425
|
+
return { status: "installed", repo };
|
|
426
|
+
}
|
|
427
|
+
// ---- file bundles ------------------------------------------------------------------
|
|
428
|
+
/**
|
|
429
|
+
* A response that carries files: an array property whose items have string
|
|
430
|
+
* `path` and `content`. typeship's own generate call is one; any API that
|
|
431
|
+
* returns generated or exported files is another. `--out <dir>` writes them.
|
|
432
|
+
*/
|
|
433
|
+
export function bundleProperty(outputSchema) {
|
|
434
|
+
const props = outputSchema?.properties;
|
|
435
|
+
if (!props)
|
|
436
|
+
return null;
|
|
437
|
+
for (const [name, schema] of Object.entries(props)) {
|
|
438
|
+
const item = schema.items;
|
|
439
|
+
if (schema.type === "array" && item?.properties?.path && item.properties.content)
|
|
440
|
+
return name;
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
/** A non-page response whose result is a collection wrapped in one array
|
|
445
|
+
* property (`{data: [...]}` is the common shape). `--fields` applies to
|
|
446
|
+
* each item while preserving that envelope, just as it does for pages. */
|
|
447
|
+
export function collectionProperty(outputSchema) {
|
|
448
|
+
const props = outputSchema?.properties;
|
|
449
|
+
const names = props ? Object.keys(props) : [];
|
|
450
|
+
if (!props || names.length !== 1)
|
|
451
|
+
return null;
|
|
452
|
+
const name = names[0];
|
|
453
|
+
const type = props[name]?.type;
|
|
454
|
+
return type === "array" || (Array.isArray(type) && type.includes("array")) ? name : null;
|
|
455
|
+
}
|
|
456
|
+
export function writeBundle(dir, files) {
|
|
457
|
+
const paths = [];
|
|
458
|
+
for (const file of files) {
|
|
459
|
+
const rel = file.path.replace(/^\/+/, "");
|
|
460
|
+
if (rel.split(/[\\/]/).includes(".."))
|
|
461
|
+
continue;
|
|
462
|
+
const full = join(dir, rel);
|
|
463
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
464
|
+
writeFileSync(full, file.content);
|
|
465
|
+
paths.push(rel);
|
|
466
|
+
}
|
|
467
|
+
return { dir, written: paths.length, paths };
|
|
468
|
+
}
|
|
469
|
+
// ---- claims ----------------------------------------------------------------------------
|
|
470
|
+
/**
|
|
471
|
+
* A response that carries a claim: an object property `claim` with a string
|
|
472
|
+
* `url` (typeship's anonymous generate is one). The CLI tells the person
|
|
473
|
+
* where to claim and leaves a breadcrumb in the working directory so a
|
|
474
|
+
* later session (or `doctor`) can list what is still unclaimed.
|
|
475
|
+
*/
|
|
476
|
+
export function claimProperty(outputSchema) {
|
|
477
|
+
const props = outputSchema?.properties;
|
|
478
|
+
const claim = props?.claim;
|
|
479
|
+
if (!claim)
|
|
480
|
+
return false;
|
|
481
|
+
const hasUrl = (v) => Boolean(v.properties && "url" in v.properties) || (v.oneOf ?? v.anyOf ?? []).some(hasUrl);
|
|
482
|
+
return hasUrl(claim);
|
|
483
|
+
}
|
|
484
|
+
export function breadcrumbFile(cwd, bin) {
|
|
485
|
+
return join(cwd, "." + bin, "claims.json");
|
|
486
|
+
}
|
|
487
|
+
/** Append a claim to .<bin>/claims.json (git-ignored when a .gitignore exists). */
|
|
488
|
+
export function recordClaim(cwd, bin, crumb) {
|
|
489
|
+
const file = breadcrumbFile(cwd, bin);
|
|
490
|
+
let list = [];
|
|
491
|
+
try {
|
|
492
|
+
list = JSON.parse(readFileSync(file, "utf8"));
|
|
493
|
+
}
|
|
494
|
+
catch { /* none yet */ }
|
|
495
|
+
list.push(crumb);
|
|
496
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
497
|
+
writeFileSync(file, JSON.stringify(list, null, 2) + "\n");
|
|
498
|
+
const gitignore = join(cwd, ".gitignore");
|
|
499
|
+
if (existsSync(gitignore)) {
|
|
500
|
+
const text = readFileSync(gitignore, "utf8");
|
|
501
|
+
const line = "." + bin + "/";
|
|
502
|
+
if (!text.split("\n").some((l) => l.trim() === line || l.trim() === "." + bin))
|
|
503
|
+
writeFileSync(gitignore, text.trimEnd() + "\n" + line + "\n");
|
|
504
|
+
}
|
|
505
|
+
return file;
|
|
506
|
+
}
|
|
507
|
+
/** Claims on file that have not expired. */
|
|
508
|
+
export function pendingClaims(cwd, bin) {
|
|
509
|
+
try {
|
|
510
|
+
const list = JSON.parse(readFileSync(breadcrumbFile(cwd, bin), "utf8"));
|
|
511
|
+
const now = Date.now();
|
|
512
|
+
return list.filter((c) => !c.expires_at || new Date(c.expires_at).getTime() > now);
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
return [];
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
export function summarizeDoctor(checks) {
|
|
519
|
+
const failing = checks.filter((c) => !c.ok);
|
|
520
|
+
return {
|
|
521
|
+
status: failing.length === 0 ? "ok" : "action_required",
|
|
522
|
+
checks,
|
|
523
|
+
next_steps: failing.map((c) => c.fix).filter((f) => Boolean(f)),
|
|
524
|
+
};
|
|
525
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|