@usecoil/skill-claude 0.1.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 +23 -0
- package/bin/commands.mjs +5 -0
- package/bin/install.mjs +699 -0
- package/package.json +48 -0
- package/skill/SKILL.md +222 -0
- package/skill/references/api-endpoints.md +139 -0
- package/skill/references/api-fields.md +202 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Coil Claude Code Skill
|
|
2
|
+
|
|
3
|
+
Installable Claude Code skill package for Coil agent workflows. It verifies the
|
|
4
|
+
installed CLI and installs the compatible public `@usecoil/cli` package when
|
|
5
|
+
`coil` is not already available:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx --yes @usecoil/skill-claude@0.1.0 --base-url https://www.usecoil.com
|
|
9
|
+
coil config set-base-url https://www.usecoil.com --profile prod
|
|
10
|
+
printf '%s' "$COIL_API_KEY" | coil auth login --profile prod --key -
|
|
11
|
+
coil --profile prod agent-context --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The installer copies the `coil-api` skill and its references into
|
|
15
|
+
`~/.claude/skills/coil-api`. It does not modify global or project `CLAUDE.md`
|
|
16
|
+
files.
|
|
17
|
+
|
|
18
|
+
Supply `COIL_API_KEY` through a secret manager or hidden prompt. The installer
|
|
19
|
+
does not accept, print, or persist authentication secrets. Do not configure
|
|
20
|
+
`COIL_ORG_ID`; Coil API keys are organization-scoped.
|
|
21
|
+
|
|
22
|
+
This package is proprietary (`UNLICENSED`). It installs a Claude Code skill,
|
|
23
|
+
not an MCP server or general Claude.ai integration.
|
package/bin/commands.mjs
ADDED
package/bin/install.mjs
ADDED
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import {
|
|
5
|
+
cpSync,
|
|
6
|
+
chmodSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
mkdtempSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
statSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from 'node:fs';
|
|
18
|
+
import { delimiter, dirname, join, resolve } from 'node:path';
|
|
19
|
+
import { homedir, tmpdir } from 'node:os';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { gunzipSync } from 'node:zlib';
|
|
22
|
+
import { spawnSyncPortable } from './commands.mjs';
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const packageMetadata = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
26
|
+
const SUPPORTED_CLI_RANGE = packageMetadata.coilCompatibility?.cli;
|
|
27
|
+
const PUBLIC_CLI_VERSION = packageMetadata.coilCompatibility?.preferredVersion;
|
|
28
|
+
const supportedCliMatch = /^(\d+)\.(\d+)\.x$/.exec(SUPPORTED_CLI_RANGE ?? '');
|
|
29
|
+
const runtimeMetadata = packageMetadata.coilRuntime;
|
|
30
|
+
|
|
31
|
+
if (!runtimeMetadata
|
|
32
|
+
|| !['codex', 'claude', 'hermes'].includes(runtimeMetadata.name)
|
|
33
|
+
|| typeof runtimeMetadata.displayName !== 'string'
|
|
34
|
+
|| !/^--[a-z]+-dir$/.test(runtimeMetadata.directoryOption ?? '')
|
|
35
|
+
|| typeof runtimeMetadata.defaultDirectory !== 'string'
|
|
36
|
+
|| typeof runtimeMetadata.skillDirectory !== 'string') {
|
|
37
|
+
console.error('Error: the skill package does not declare a valid runtime install contract.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!supportedCliMatch) {
|
|
42
|
+
console.error('Error: the skill package does not declare a valid Coil CLI compatibility range.');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isolatedNpmConfigDir = mkdtempSync(join(tmpdir(), 'coil-npm-config-'));
|
|
47
|
+
const isolatedNpmUserConfig = join(isolatedNpmConfigDir, 'user.npmrc');
|
|
48
|
+
const isolatedNpmGlobalConfig = join(isolatedNpmConfigDir, 'global.npmrc');
|
|
49
|
+
writeFileSync(isolatedNpmUserConfig, '');
|
|
50
|
+
writeFileSync(isolatedNpmGlobalConfig, '');
|
|
51
|
+
const cleanupDirs = [isolatedNpmConfigDir];
|
|
52
|
+
process.on('exit', () => {
|
|
53
|
+
for (const path of cleanupDirs) rmSync(path, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const SUPPORTED_CLI_VERSION = new RegExp(
|
|
57
|
+
`^v?${supportedCliMatch[1]}\\.${supportedCliMatch[2]}\\.\\d+(?:-[0-9A-Za-z.-]+)?$`,
|
|
58
|
+
);
|
|
59
|
+
if (!SUPPORTED_CLI_VERSION.test(PUBLIC_CLI_VERSION ?? '')) {
|
|
60
|
+
console.error('Error: the skill package must declare a preferred compatible Coil CLI version.');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
const PUBLIC_CLI_SPEC = `@usecoil/cli@${PUBLIC_CLI_VERSION}`;
|
|
64
|
+
const defaultNpmPrefix = process.env.npm_config_prefix || process.env.NPM_CONFIG_PREFIX || join(homedir(), '.local');
|
|
65
|
+
const runtimeDefaultDir = join(homedir(), runtimeMetadata.defaultDirectory);
|
|
66
|
+
|
|
67
|
+
function globalBinDir() {
|
|
68
|
+
return process.platform === 'win32' ? defaultNpmPrefix : join(defaultNpmPrefix, 'bin');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (Number.parseInt(process.versions.node.split('.')[0], 10) < 20) {
|
|
72
|
+
console.error('Error: Coil setup requires Node.js 20 or newer.');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseArgs(argv) {
|
|
77
|
+
const args = {};
|
|
78
|
+
for (let i = 2; i < argv.length; i += 1) {
|
|
79
|
+
const arg = argv[i];
|
|
80
|
+
if (arg === '--profile' && argv[i + 1]) args.profile = argv[++i];
|
|
81
|
+
else if (arg === '--base-url' && argv[i + 1]) args.baseUrl = argv[++i];
|
|
82
|
+
else if (arg === runtimeMetadata.directoryOption && argv[i + 1]) args.runtimeDir = argv[++i];
|
|
83
|
+
else if (arg === '--cli-package' && argv[i + 1]) args.cliPackage = argv[++i];
|
|
84
|
+
else if (arg === '--cli-sha256' && argv[i + 1]) args.cliSha256 = argv[++i];
|
|
85
|
+
else if (arg === '--help' || arg === '-h') args.help = true;
|
|
86
|
+
else {
|
|
87
|
+
console.error(`Unknown option: ${arg}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return args;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function printUsage() {
|
|
95
|
+
console.log(`
|
|
96
|
+
Usage: npx ${packageMetadata.name} [options]
|
|
97
|
+
|
|
98
|
+
Options:
|
|
99
|
+
--profile <name> Suggested Coil auth profile name. Defaults to "coil".
|
|
100
|
+
--base-url <url> Coil API base URL to include in installed setup guidance.
|
|
101
|
+
Defaults to COIL_BASE_URL when present.
|
|
102
|
+
${runtimeMetadata.directoryOption} <path>${' '.repeat(Math.max(1, 25 - runtimeMetadata.directoryOption.length))}${runtimeMetadata.displayName} config directory. Defaults to ~/${runtimeMetadata.defaultDirectory}.
|
|
103
|
+
--cli-package <spec> Use an exact @usecoil/cli 0.1.x version or local .tgz instead
|
|
104
|
+
of the public npm package when coil is absent.
|
|
105
|
+
--cli-sha256 <hex> Required with a local --cli-package tarball; verify SHA-256 first.
|
|
106
|
+
--help Show this help message.
|
|
107
|
+
|
|
108
|
+
Default install:
|
|
109
|
+
- Verifies a compatible Coil CLI (0.1.x), installing ${PUBLIC_CLI_SPEC} when absent
|
|
110
|
+
- Atomically installs the coil-api skill and API references
|
|
111
|
+
${runtimeMetadata.guidanceFile
|
|
112
|
+
? ` - Writes general Coil guidance to ~/${runtimeMetadata.defaultDirectory}/${runtimeMetadata.guidanceFile}`
|
|
113
|
+
: ` - Leaves global ${runtimeMetadata.displayName} instruction files unchanged`}
|
|
114
|
+
|
|
115
|
+
Authentication is normally done with:
|
|
116
|
+
coil auth login
|
|
117
|
+
${agentAuthCommand('<name>')}
|
|
118
|
+
coil --profile <name> agent-context --json
|
|
119
|
+
|
|
120
|
+
The installer never accepts, prints, or persists authentication secrets.
|
|
121
|
+
`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function stripFrontmatter(markdown) {
|
|
125
|
+
return markdown.replace(/^---[\s\S]*?---\n*/, '');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function shellQuote(value) {
|
|
129
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function agentAuthCommand(profile) {
|
|
133
|
+
return process.platform === 'win32'
|
|
134
|
+
? `$env:COIL_API_KEY | coil auth login --profile ${shellQuote(profile)} --key -`
|
|
135
|
+
: `printf '%s' "$COIL_API_KEY" | coil auth login --profile ${shellQuote(profile)} --key -`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function upsertSection(existing, heading, body) {
|
|
139
|
+
const section = `${heading}\n\n${body.trim()}\n`;
|
|
140
|
+
if (!existing) return section;
|
|
141
|
+
|
|
142
|
+
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
143
|
+
const pattern = new RegExp(`${escapedHeading}[\\s\\S]*?(?=\\n# |$)`);
|
|
144
|
+
if (pattern.test(existing)) {
|
|
145
|
+
return existing.replace(pattern, section.trimEnd()) + (existing.endsWith('\n') ? '' : '\n');
|
|
146
|
+
}
|
|
147
|
+
return `${existing.trimEnd()}\n\n${section}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function validateBaseUrl(value) {
|
|
151
|
+
if (!value) return undefined;
|
|
152
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) throw new Error('--base-url cannot contain control characters.');
|
|
153
|
+
let parsed;
|
|
154
|
+
try {
|
|
155
|
+
parsed = new URL(value);
|
|
156
|
+
} catch {
|
|
157
|
+
throw new Error('--base-url must be an absolute HTTP(S) URL.');
|
|
158
|
+
}
|
|
159
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname) {
|
|
160
|
+
throw new Error('--base-url must be an absolute HTTP(S) URL.');
|
|
161
|
+
}
|
|
162
|
+
if (parsed.username || parsed.password) throw new Error('--base-url cannot contain credentials.');
|
|
163
|
+
if (parsed.search || parsed.hash) {
|
|
164
|
+
throw new Error('--base-url cannot contain a query string or fragment.');
|
|
165
|
+
}
|
|
166
|
+
const localHosts = new Set(['localhost', '127.0.0.1', '::1']);
|
|
167
|
+
if (parsed.protocol === 'http:' && !localHosts.has(parsed.hostname) && !parsed.hostname.endsWith('.localhost')) {
|
|
168
|
+
throw new Error('--base-url must use HTTPS unless it targets loopback.');
|
|
169
|
+
}
|
|
170
|
+
return value.replace(/\/$/, '');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function validateCliPackage(value) {
|
|
174
|
+
const exactPackagePattern = new RegExp(
|
|
175
|
+
`^@usecoil/cli@${supportedCliMatch[1]}\\.${supportedCliMatch[2]}\\.\\d+(?:-[0-9A-Za-z.-]+)?$`,
|
|
176
|
+
);
|
|
177
|
+
if (exactPackagePattern.test(value)) return value;
|
|
178
|
+
const candidate = resolve(value.replace(/^file:/, ''));
|
|
179
|
+
if (candidate.endsWith('.tgz') && existsSync(candidate) && statSync(candidate).isFile()) return candidate;
|
|
180
|
+
throw new Error('--cli-package must be an exact @usecoil/cli 0.1.x version or an existing local .tgz file.');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function validateCliSha256(value) {
|
|
184
|
+
if (!/^[a-f0-9]{64}$/i.test(value)) {
|
|
185
|
+
throw new Error('--cli-sha256 must be a 64-character hexadecimal SHA-256 digest.');
|
|
186
|
+
}
|
|
187
|
+
return value.toLowerCase();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isLocalCliPackage(value) {
|
|
191
|
+
return value.endsWith('.tgz') && existsSync(value) && statSync(value).isFile();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function spawnPortable(command, args, options = {}) {
|
|
195
|
+
return spawnSyncPortable(command, args, options);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function snapshotLocalCliPackage(path, expectedSha256) {
|
|
199
|
+
const packageBytes = readFileSync(path);
|
|
200
|
+
const actualSha256 = createHash('sha256').update(packageBytes).digest('hex');
|
|
201
|
+
if (actualSha256 !== expectedSha256) {
|
|
202
|
+
throw new Error(`--cli-package SHA-256 mismatch; expected ${expectedSha256}, received ${actualSha256}.`);
|
|
203
|
+
}
|
|
204
|
+
validateLocalCliPackageMetadata(packageBytes);
|
|
205
|
+
const snapshotDir = mkdtempSync(join(tmpdir(), 'coil-cli-package-'));
|
|
206
|
+
cleanupDirs.push(snapshotDir);
|
|
207
|
+
chmodSync(snapshotDir, 0o700);
|
|
208
|
+
const snapshotPath = join(snapshotDir, 'usecoil-cli.tgz');
|
|
209
|
+
writeFileSync(snapshotPath, packageBytes, { flag: 'wx', mode: 0o600 });
|
|
210
|
+
return snapshotPath;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function readTarEntry(archive, targetName) {
|
|
214
|
+
for (let offset = 0; offset + 512 <= archive.length; ) {
|
|
215
|
+
const header = archive.subarray(offset, offset + 512);
|
|
216
|
+
if (header.every((byte) => byte === 0)) break;
|
|
217
|
+
const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, '');
|
|
218
|
+
const sizeText = header.subarray(124, 136).toString('ascii').replace(/\0.*$/, '').trim();
|
|
219
|
+
const size = sizeText ? Number.parseInt(sizeText, 8) : 0;
|
|
220
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new Error('local CLI package contains an invalid tar entry size.');
|
|
221
|
+
const contentStart = offset + 512;
|
|
222
|
+
if (name === targetName) return archive.subarray(contentStart, contentStart + size).toString('utf8');
|
|
223
|
+
offset = contentStart + Math.ceil(size / 512) * 512;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function validateLocalCliPackageMetadata(packageBytes) {
|
|
229
|
+
let metadata;
|
|
230
|
+
try {
|
|
231
|
+
const packageJson = readTarEntry(gunzipSync(packageBytes), 'package/package.json');
|
|
232
|
+
if (!packageJson) throw new Error('package/package.json is missing.');
|
|
233
|
+
metadata = JSON.parse(packageJson);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw new Error(`--cli-package is not a readable npm package: ${error.message}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const expectedBin = metadata.bin?.coil === 'dist/coil.js' || metadata.bin?.coil === './dist/coil.js';
|
|
239
|
+
const expectedExports = JSON.stringify(metadata.exports) === JSON.stringify({ './package.json': './package.json' });
|
|
240
|
+
const valid = metadata.name === '@usecoil/cli'
|
|
241
|
+
&& SUPPORTED_CLI_VERSION.test(String(metadata.version ?? ''))
|
|
242
|
+
&& metadata.license === 'UNLICENSED'
|
|
243
|
+
&& metadata.engines?.node === '>=20'
|
|
244
|
+
&& expectedBin
|
|
245
|
+
&& expectedExports
|
|
246
|
+
&& metadata.publishConfig?.access === 'public'
|
|
247
|
+
&& !metadata.main
|
|
248
|
+
&& !metadata.dependencies
|
|
249
|
+
&& !metadata.devDependencies;
|
|
250
|
+
if (!valid) {
|
|
251
|
+
throw new Error('--cli-package metadata must be the reviewed @usecoil/cli public executable package.');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function checkCli() {
|
|
256
|
+
const pathExecutable = resolveCoilOnPath();
|
|
257
|
+
const packageCheck = pathExecutable
|
|
258
|
+
? verifyInstalledCliPackage(packagePathForExecutable(pathExecutable))
|
|
259
|
+
: verifyInstalledCliPackage();
|
|
260
|
+
if (!packageCheck.ok) {
|
|
261
|
+
return {
|
|
262
|
+
ok: false,
|
|
263
|
+
present: Boolean(pathExecutable || globalCliPackagePath()),
|
|
264
|
+
version: null,
|
|
265
|
+
reason: pathExecutable
|
|
266
|
+
? 'A coil executable is present on PATH but is not owned by the reviewed global @usecoil/cli package.'
|
|
267
|
+
: packageCheck.reason,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const probeHome = mkdtempSync(join(tmpdir(), 'coil-cli-probe-'));
|
|
271
|
+
const probeEnvironment = sanitizedCliEnvironment(probeHome);
|
|
272
|
+
const result = spawnPortable(process.execPath, [packageCheck.entrypoint, '--version'], {
|
|
273
|
+
encoding: 'utf8',
|
|
274
|
+
env: probeEnvironment,
|
|
275
|
+
});
|
|
276
|
+
rmSync(probeHome, { recursive: true, force: true });
|
|
277
|
+
if (result.error?.code === 'ENOENT') return { ok: false, present: false, version: null, reason: null };
|
|
278
|
+
if (result.error || result.status !== 0) {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
present: true,
|
|
282
|
+
version: null,
|
|
283
|
+
reason: 'A coil executable is present but failed its version check.',
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const version = result.stdout.trim();
|
|
287
|
+
if (!SUPPORTED_CLI_VERSION.test(version)) return { ok: false, present: true, version, reason: null };
|
|
288
|
+
|
|
289
|
+
const discoveryHome = mkdtempSync(join(tmpdir(), 'coil-cli-discovery-'));
|
|
290
|
+
try {
|
|
291
|
+
const contextResult = spawnPortable(process.execPath, [packageCheck.entrypoint, '--json', 'agent-context'], {
|
|
292
|
+
encoding: 'utf8',
|
|
293
|
+
env: sanitizedCliEnvironment(discoveryHome),
|
|
294
|
+
});
|
|
295
|
+
const context = JSON.parse(contextResult.stdout);
|
|
296
|
+
const activationCommands = context.commands?.activation?.commands;
|
|
297
|
+
const activationStatus = Array.isArray(activationCommands)
|
|
298
|
+
&& activationCommands.some((command) => command?.name === 'activation status');
|
|
299
|
+
const starterRun = Array.isArray(activationCommands)
|
|
300
|
+
&& activationCommands.find((command) => command?.name === 'recipes run --starter-run');
|
|
301
|
+
const starterParams = Array.isArray(starterRun?.params) ? starterRun.params : [];
|
|
302
|
+
const hasSpendGate = starterParams.some((param) => (
|
|
303
|
+
param?.name === '--confirm-provider-spend' && param?.type === 'boolean' && param?.required === true
|
|
304
|
+
));
|
|
305
|
+
const fetchCount = starterParams.find((param) => param?.name === '--fetch-count');
|
|
306
|
+
const hasStarterCap = fetchCount?.type === 'number'
|
|
307
|
+
&& fetchCount?.required === true
|
|
308
|
+
&& fetchCount?.minimum === 1
|
|
309
|
+
&& fetchCount?.maximum === 25;
|
|
310
|
+
const compatible = contextResult.status === 0
|
|
311
|
+
&& context.cli?.name === 'coil'
|
|
312
|
+
&& context.cli?.version === version.replace(/^v/, '')
|
|
313
|
+
&& activationStatus
|
|
314
|
+
&& hasSpendGate
|
|
315
|
+
&& hasStarterCap
|
|
316
|
+
&& Array.isArray(context.commands?.recipes?.commands)
|
|
317
|
+
&& context.commands.recipes.commands.some((command) => command?.name === 'recipes run');
|
|
318
|
+
return {
|
|
319
|
+
ok: Boolean(compatible),
|
|
320
|
+
present: true,
|
|
321
|
+
version,
|
|
322
|
+
reason: compatible ? null : 'The executable does not expose the required Coil runtime-discovery contract.',
|
|
323
|
+
};
|
|
324
|
+
} catch {
|
|
325
|
+
return {
|
|
326
|
+
ok: false,
|
|
327
|
+
present: true,
|
|
328
|
+
version,
|
|
329
|
+
reason: 'The executable does not return valid Coil runtime-discovery JSON.',
|
|
330
|
+
};
|
|
331
|
+
} finally {
|
|
332
|
+
rmSync(discoveryHome, { recursive: true, force: true });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function rollbackCliInstall() {
|
|
337
|
+
const rollback = spawnPortable('npm', [
|
|
338
|
+
'uninstall',
|
|
339
|
+
'--global',
|
|
340
|
+
'--ignore-scripts',
|
|
341
|
+
'--no-audit',
|
|
342
|
+
'--no-fund',
|
|
343
|
+
'@usecoil/cli',
|
|
344
|
+
], { encoding: 'utf8', env: sanitizedNpmEnvironment() });
|
|
345
|
+
return !rollback.error && rollback.status === 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function globalCliPackagePath() {
|
|
349
|
+
const rootResult = spawnPortable('npm', ['root', '--global'], { encoding: 'utf8', env: sanitizedNpmEnvironment() });
|
|
350
|
+
if (rootResult.error || rootResult.status !== 0) return null;
|
|
351
|
+
const packagePath = join(rootResult.stdout.trim(), '@usecoil', 'cli', 'package.json');
|
|
352
|
+
return existsSync(packagePath) ? packagePath : null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function resolveCoilOnPath() {
|
|
356
|
+
const pathEntries = (process.env.PATH ?? '').split(delimiter).filter(Boolean);
|
|
357
|
+
const names = process.platform === 'win32'
|
|
358
|
+
? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').map((ext) => `coil${ext.toLowerCase()}`)
|
|
359
|
+
: ['coil'];
|
|
360
|
+
for (const pathEntry of pathEntries) {
|
|
361
|
+
for (const name of names) {
|
|
362
|
+
const candidate = resolve(pathEntry, name);
|
|
363
|
+
try {
|
|
364
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
365
|
+
} catch {
|
|
366
|
+
// Continue searching PATH.
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function packagePathForExecutable(executable) {
|
|
374
|
+
if (process.platform === 'win32') {
|
|
375
|
+
return join(dirname(executable), 'node_modules', '@usecoil', 'cli', 'package.json');
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const entrypoint = realpathSync(executable);
|
|
379
|
+
return join(dirname(dirname(entrypoint)), 'package.json');
|
|
380
|
+
} catch {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function verifyInstalledCliPackage(packagePath = globalCliPackagePath()) {
|
|
386
|
+
if (!packagePath) return { ok: false, reason: 'Could not locate the global npm package root after installation.' };
|
|
387
|
+
try {
|
|
388
|
+
const metadata = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
389
|
+
const expectedBin = metadata.bin?.coil === 'dist/coil.js' || metadata.bin?.coil === './dist/coil.js';
|
|
390
|
+
const expectedExports = JSON.stringify(metadata.exports) === JSON.stringify({ './package.json': './package.json' });
|
|
391
|
+
const valid = metadata.name === '@usecoil/cli'
|
|
392
|
+
&& SUPPORTED_CLI_VERSION.test(String(metadata.version ?? ''))
|
|
393
|
+
&& metadata.license === 'UNLICENSED'
|
|
394
|
+
&& metadata.engines?.node === '>=20'
|
|
395
|
+
&& expectedBin
|
|
396
|
+
&& expectedExports
|
|
397
|
+
&& !metadata.main
|
|
398
|
+
&& !metadata.dependencies
|
|
399
|
+
&& !metadata.devDependencies;
|
|
400
|
+
const entrypoint = join(dirname(packagePath), 'dist', 'coil.js');
|
|
401
|
+
const entrypointIsFile = existsSync(entrypoint) && statSync(entrypoint).isFile();
|
|
402
|
+
return valid && entrypointIsFile
|
|
403
|
+
? { ok: true, reason: null, entrypoint }
|
|
404
|
+
: { ok: false, reason: 'The installed package metadata does not match the reviewed Coil CLI contract.' };
|
|
405
|
+
} catch {
|
|
406
|
+
return { ok: false, reason: 'The installed Coil CLI package metadata could not be read.' };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function sanitizedNpmEnvironment() {
|
|
411
|
+
const allowed = new Set([
|
|
412
|
+
'appdata',
|
|
413
|
+
'comspec',
|
|
414
|
+
'home',
|
|
415
|
+
'homedrive',
|
|
416
|
+
'homepath',
|
|
417
|
+
'lang',
|
|
418
|
+
'lc_all',
|
|
419
|
+
'lc_ctype',
|
|
420
|
+
'localappdata',
|
|
421
|
+
'path',
|
|
422
|
+
'pathext',
|
|
423
|
+
'systemroot',
|
|
424
|
+
'temp',
|
|
425
|
+
'tmp',
|
|
426
|
+
'tmpdir',
|
|
427
|
+
'userprofile',
|
|
428
|
+
]);
|
|
429
|
+
const environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => {
|
|
430
|
+
const normalized = key.toLowerCase();
|
|
431
|
+
return allowed.has(normalized) || normalized === 'npm_config_prefix' || normalized === 'npm_config_cache';
|
|
432
|
+
}).concat([
|
|
433
|
+
['npm_config_prefix', defaultNpmPrefix],
|
|
434
|
+
['npm_config_userconfig', isolatedNpmUserConfig],
|
|
435
|
+
['npm_config_globalconfig', isolatedNpmGlobalConfig],
|
|
436
|
+
]));
|
|
437
|
+
|
|
438
|
+
for (const key of [
|
|
439
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY',
|
|
440
|
+
'http_proxy', 'https_proxy', 'no_proxy',
|
|
441
|
+
'npm_config_proxy', 'npm_config_https_proxy', 'npm_config_noproxy', 'npm_config_registry',
|
|
442
|
+
]) {
|
|
443
|
+
const value = process.env[key];
|
|
444
|
+
if (!value) continue;
|
|
445
|
+
if (key.toLowerCase().includes('proxy') || key === 'npm_config_registry') {
|
|
446
|
+
try {
|
|
447
|
+
const parsed = new URL(value);
|
|
448
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password) continue;
|
|
449
|
+
} catch {
|
|
450
|
+
const normalized = key.toLowerCase();
|
|
451
|
+
if (normalized !== 'no_proxy' && normalized !== 'npm_config_noproxy') continue;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
environment[key] = value;
|
|
455
|
+
}
|
|
456
|
+
for (const key of ['npm_config_cafile', 'NODE_EXTRA_CA_CERTS']) {
|
|
457
|
+
if (process.env[key]) environment[key] = process.env[key];
|
|
458
|
+
}
|
|
459
|
+
return environment;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function sanitizedCliEnvironment(isolatedHome) {
|
|
463
|
+
const environment = sanitizedNpmEnvironment();
|
|
464
|
+
environment.HOME = isolatedHome;
|
|
465
|
+
environment.USERPROFILE = isolatedHome;
|
|
466
|
+
environment.COIL_API_KEY = '';
|
|
467
|
+
environment.COIL_PROFILE = '';
|
|
468
|
+
environment.COIL_BASE_URL = 'http://127.0.0.1:9';
|
|
469
|
+
environment.PATH = [globalBinDir(), environment.PATH].filter(Boolean).join(process.platform === 'win32' ? ';' : ':');
|
|
470
|
+
return environment;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function maybeInjectFailure(step) {
|
|
474
|
+
if (process.env.NODE_ENV === 'test' && process.env.COIL_SKILL_INSTALL_TEST_FAIL_AFTER === step) {
|
|
475
|
+
throw new Error(`Injected installer failure after ${step}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function lstatIfPresent(path) {
|
|
480
|
+
try {
|
|
481
|
+
return lstatSync(path);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (error?.code === 'ENOENT') return null;
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const args = parseArgs(process.argv);
|
|
489
|
+
args.profile ??= 'coil';
|
|
490
|
+
args.baseUrl ??= process.env.COIL_BASE_URL;
|
|
491
|
+
args.runtimeDir ??= runtimeDefaultDir;
|
|
492
|
+
|
|
493
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(args.profile)) {
|
|
494
|
+
console.error('Error: --profile must be 1-64 letters, numbers, dots, underscores, or hyphens.');
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
args.baseUrl = validateBaseUrl(args.baseUrl);
|
|
500
|
+
if (args.cliPackage) {
|
|
501
|
+
args.cliPackage = validateCliPackage(args.cliPackage);
|
|
502
|
+
if (isLocalCliPackage(args.cliPackage)) {
|
|
503
|
+
if (!args.cliSha256) throw new Error('--cli-sha256 is required when --cli-package points to a local .tgz file.');
|
|
504
|
+
args.cliSha256 = validateCliSha256(args.cliSha256);
|
|
505
|
+
args.cliPackage = snapshotLocalCliPackage(args.cliPackage, args.cliSha256);
|
|
506
|
+
} else if (args.cliSha256) {
|
|
507
|
+
throw new Error('--cli-sha256 is only valid with a local --cli-package .tgz file.');
|
|
508
|
+
}
|
|
509
|
+
} else if (args.cliSha256) {
|
|
510
|
+
throw new Error('--cli-sha256 requires --cli-package.');
|
|
511
|
+
}
|
|
512
|
+
} catch (error) {
|
|
513
|
+
console.error(`Error: ${error.message}`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (args.help) {
|
|
518
|
+
printUsage();
|
|
519
|
+
process.exit(0);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
let cli = checkCli();
|
|
523
|
+
let installedCli = false;
|
|
524
|
+
if (!cli.ok && cli.present) {
|
|
525
|
+
const found = cli.version ? ` ${cli.version}` : '';
|
|
526
|
+
const detail = cli.reason ?? `Found${found}; supported versions are ${SUPPORTED_CLI_RANGE}.`;
|
|
527
|
+
console.error(`Error: ${detail} Refusing to replace an existing executable automatically.`);
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!cli.ok) {
|
|
532
|
+
if (globalCliPackagePath()) {
|
|
533
|
+
console.error('Error: @usecoil/cli is already installed globally but its coil executable is unavailable; refusing to replace the existing package automatically.');
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
const installSpec = args.cliPackage ?? PUBLIC_CLI_SPEC;
|
|
537
|
+
const install = spawnPortable('npm', [
|
|
538
|
+
'install',
|
|
539
|
+
'--global',
|
|
540
|
+
'--ignore-scripts',
|
|
541
|
+
'--no-audit',
|
|
542
|
+
'--no-fund',
|
|
543
|
+
installSpec,
|
|
544
|
+
], { encoding: 'utf8', env: sanitizedNpmEnvironment() });
|
|
545
|
+
if (install.error || install.status !== 0) {
|
|
546
|
+
const source = args.cliPackage ? 'the reviewed Coil CLI package' : `public npm package ${PUBLIC_CLI_SPEC}`;
|
|
547
|
+
console.error(`Error: ${source} could not be installed.`);
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
installedCli = true;
|
|
551
|
+
const packageCheck = verifyInstalledCliPackage();
|
|
552
|
+
if (!packageCheck.ok) {
|
|
553
|
+
const rollbackFailed = !rollbackCliInstall();
|
|
554
|
+
console.error(`Error: ${packageCheck.reason}`);
|
|
555
|
+
if (rollbackFailed) console.error('Warning: automatic rollback of the newly installed Coil CLI failed; remove @usecoil/cli manually.');
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
cli = checkCli();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (!cli.ok) {
|
|
562
|
+
const rollbackFailed = installedCli && !rollbackCliInstall();
|
|
563
|
+
const detail = cli.reason ?? (cli.version ? `Found ${cli.version}; supported versions are ${SUPPORTED_CLI_RANGE}.` : 'No coil executable was found.');
|
|
564
|
+
console.error(`Error: ${detail} Install @usecoil/cli ${PUBLIC_CLI_SPEC} from the public npm registry, or rerun with --cli-package <path-to-reviewed-tarball>.`);
|
|
565
|
+
if (rollbackFailed) console.error('Warning: automatic rollback of the newly installed Coil CLI failed; remove @usecoil/cli manually.');
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const runtimeDir = args.runtimeDir;
|
|
570
|
+
const skillSource = join(__dirname, '..', 'skill');
|
|
571
|
+
const skillDest = join(runtimeDir, ...runtimeMetadata.skillDirectory.split('/'));
|
|
572
|
+
const referencesDest = runtimeMetadata.referencesDirectory
|
|
573
|
+
? join(runtimeDir, ...runtimeMetadata.referencesDirectory.split('/'))
|
|
574
|
+
: null;
|
|
575
|
+
const guidanceDest = runtimeMetadata.guidanceFile ? join(runtimeDir, runtimeMetadata.guidanceFile) : null;
|
|
576
|
+
|
|
577
|
+
let stagingRoot;
|
|
578
|
+
try {
|
|
579
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
580
|
+
stagingRoot = mkdtempSync(join(runtimeDir, '.coil-install-'));
|
|
581
|
+
} catch (error) {
|
|
582
|
+
const rollbackFailed = installedCli && !rollbackCliInstall();
|
|
583
|
+
console.error(`Error: Coil skill installation could not create its staging directory: ${error.message}`);
|
|
584
|
+
if (rollbackFailed) console.error('Warning: automatic rollback of the newly installed Coil CLI failed; remove @usecoil/cli manually.');
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
const stagedRoot = join(stagingRoot, 'new');
|
|
588
|
+
const backupRoot = join(stagingRoot, 'backup');
|
|
589
|
+
const stagedSkill = join(stagedRoot, 'coil-api');
|
|
590
|
+
const stagedReferences = referencesDest ? join(stagedRoot, 'coil-references') : null;
|
|
591
|
+
const stagedGuidance = guidanceDest ? join(stagedRoot, runtimeMetadata.guidanceFile) : null;
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
mkdirSync(stagedRoot, { recursive: true });
|
|
595
|
+
mkdirSync(backupRoot, { recursive: true });
|
|
596
|
+
cpSync(skillSource, stagedSkill, { recursive: true });
|
|
597
|
+
if (stagedReferences) {
|
|
598
|
+
mkdirSync(stagedReferences, { recursive: true });
|
|
599
|
+
for (const file of ['api-endpoints.md', 'api-fields.md']) {
|
|
600
|
+
cpSync(join(skillSource, 'references', file), join(stagedReferences, file));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (stagedGuidance) {
|
|
605
|
+
const skillBody = stripFrontmatter(readFileSync(join(skillSource, 'SKILL.md'), 'utf-8'));
|
|
606
|
+
const guidanceBody = `${skillBody}
|
|
607
|
+
|
|
608
|
+
## Local Install Notes
|
|
609
|
+
|
|
610
|
+
- Human auth: \`coil auth login\`
|
|
611
|
+
- Agent auth: \`${agentAuthCommand(args.profile)}\`
|
|
612
|
+
- API base URL: \`${args.baseUrl
|
|
613
|
+
? `coil config set-base-url ${shellQuote(args.baseUrl)} --profile ${shellQuote(args.profile)}`
|
|
614
|
+
: `coil config set-base-url https://www.usecoil.com --profile ${shellQuote(args.profile)}`}\`
|
|
615
|
+
- First discovery command: \`coil --profile ${shellQuote(args.profile)} agent-context --json\`
|
|
616
|
+
- Compatible CLI: \`${SUPPORTED_CLI_RANGE}\`
|
|
617
|
+
- Env fallback: \`COIL_API_KEY\` and \`COIL_BASE_URL\`
|
|
618
|
+
- Do not set \`COIL_ORG_ID\`; Coil API keys are organization-scoped.
|
|
619
|
+
`;
|
|
620
|
+
let existingGuidance = '';
|
|
621
|
+
let guidanceMode = 0o600;
|
|
622
|
+
if (existsSync(guidanceDest)) {
|
|
623
|
+
const guidanceStat = lstatSync(guidanceDest);
|
|
624
|
+
if (guidanceStat.isSymbolicLink() || !guidanceStat.isFile()) {
|
|
625
|
+
throw new Error(`${runtimeMetadata.guidanceFile} must be a regular file, not a symlink or special file.`);
|
|
626
|
+
}
|
|
627
|
+
existingGuidance = readFileSync(guidanceDest, 'utf8');
|
|
628
|
+
guidanceMode = guidanceStat.mode & 0o777;
|
|
629
|
+
}
|
|
630
|
+
writeFileSync(stagedGuidance, upsertSection(existingGuidance, '# Coil Integration', guidanceBody), { mode: guidanceMode });
|
|
631
|
+
chmodSync(stagedGuidance, guidanceMode);
|
|
632
|
+
}
|
|
633
|
+
} catch (error) {
|
|
634
|
+
rmSync(stagingRoot, { recursive: true, force: true });
|
|
635
|
+
const rollbackFailed = installedCli && !rollbackCliInstall();
|
|
636
|
+
console.error(`Error: Coil skill installation could not be staged: ${error.message}`);
|
|
637
|
+
if (rollbackFailed) console.error('Warning: automatic rollback of the newly installed Coil CLI failed; remove @usecoil/cli manually.');
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const targets = [
|
|
642
|
+
{ name: 'skill', staged: stagedSkill, target: skillDest, backup: join(backupRoot, 'coil-api') },
|
|
643
|
+
...(stagedReferences ? [{ name: 'references', staged: stagedReferences, target: referencesDest, backup: join(backupRoot, 'coil-references') }] : []),
|
|
644
|
+
...(stagedGuidance ? [{ name: 'guidance', staged: stagedGuidance, target: guidanceDest, backup: join(backupRoot, runtimeMetadata.guidanceFile) }] : []),
|
|
645
|
+
];
|
|
646
|
+
const applied = [];
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
for (const item of targets) {
|
|
650
|
+
mkdirSync(dirname(item.target), { recursive: true });
|
|
651
|
+
const previousStat = lstatIfPresent(item.target);
|
|
652
|
+
if (previousStat?.isSymbolicLink()
|
|
653
|
+
|| (item.name === 'guidance' ? previousStat && !previousStat.isFile() : previousStat && !previousStat.isDirectory())) {
|
|
654
|
+
throw new Error(`${item.target} must be a regular ${item.name === 'guidance' ? 'file' : 'directory'}, not a symlink or special file.`);
|
|
655
|
+
}
|
|
656
|
+
const hadPrevious = Boolean(previousStat);
|
|
657
|
+
if (hadPrevious) renameSync(item.target, item.backup);
|
|
658
|
+
try {
|
|
659
|
+
renameSync(item.staged, item.target);
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (hadPrevious && existsSync(item.backup)) renameSync(item.backup, item.target);
|
|
662
|
+
throw error;
|
|
663
|
+
}
|
|
664
|
+
applied.push({ ...item, hadPrevious });
|
|
665
|
+
maybeInjectFailure(item.name);
|
|
666
|
+
}
|
|
667
|
+
rmSync(stagingRoot, { recursive: true, force: true });
|
|
668
|
+
} catch (error) {
|
|
669
|
+
for (const item of applied.reverse()) {
|
|
670
|
+
rmSync(item.target, { recursive: true, force: true });
|
|
671
|
+
if (item.hadPrevious && existsSync(item.backup)) renameSync(item.backup, item.target);
|
|
672
|
+
}
|
|
673
|
+
rmSync(stagingRoot, { recursive: true, force: true });
|
|
674
|
+
const rollbackFailed = installedCli && !rollbackCliInstall();
|
|
675
|
+
console.error(`Error: Coil skill installation rolled back: ${error.message}`);
|
|
676
|
+
if (rollbackFailed) console.error('Warning: automatic rollback of the newly installed Coil CLI failed; remove @usecoil/cli manually.');
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
console.log(`Verified Coil CLI ${cli.version} (${SUPPORTED_CLI_RANGE}).`);
|
|
681
|
+
if (installedCli && !(process.env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').includes(globalBinDir())) {
|
|
682
|
+
const guidance = process.platform === 'win32'
|
|
683
|
+
? `Add ${globalBinDir()} to PATH before running coil in a new terminal.`
|
|
684
|
+
: `Run: export PATH=${shellQuote(`${globalBinDir()}:$PATH`)}`;
|
|
685
|
+
console.log(guidance);
|
|
686
|
+
}
|
|
687
|
+
console.log(`Installed Coil skill to ${skillDest}`);
|
|
688
|
+
if (referencesDest) console.log(`Installed Coil API references to ${referencesDest}`);
|
|
689
|
+
if (guidanceDest) console.log(`Wrote Coil guidance to ${guidanceDest}`);
|
|
690
|
+
console.log('Human auth: coil auth login');
|
|
691
|
+
console.log(`Agent auth: ${agentAuthCommand(args.profile)}`);
|
|
692
|
+
|
|
693
|
+
if (args.baseUrl) {
|
|
694
|
+
console.log(`Required base URL setup: coil config set-base-url ${shellQuote(args.baseUrl)} --profile ${shellQuote(args.profile)}`);
|
|
695
|
+
} else {
|
|
696
|
+
console.log(`Required public-service setup: coil config set-base-url https://www.usecoil.com --profile ${shellQuote(args.profile)}`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
console.log(`Done. After base URL setup and authentication, run coil --profile ${shellQuote(args.profile)} agent-context --json.`);
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usecoil/skill-claude",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code skill package for Coil agent workflows",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"coil-skill-claude": "./bin/install.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"skill",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"coil",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"agents",
|
|
18
|
+
"lead-generation"
|
|
19
|
+
],
|
|
20
|
+
"license": "UNLICENSED",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/antons-agents/coil.git",
|
|
24
|
+
"directory": "packages/skill-claude"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"coilCompatibility": {
|
|
30
|
+
"cli": "0.1.x",
|
|
31
|
+
"preferredVersion": "0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"coilRuntime": {
|
|
34
|
+
"name": "claude",
|
|
35
|
+
"displayName": "Claude Code",
|
|
36
|
+
"directoryOption": "--claude-dir",
|
|
37
|
+
"defaultDirectory": ".claude",
|
|
38
|
+
"skillDirectory": "skills/coil-api",
|
|
39
|
+
"referencesDirectory": null,
|
|
40
|
+
"guidanceFile": null
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"cross-spawn": "7.0.6"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: coil-api
|
|
3
|
+
description: Use Coil's CLI and API for agent-operated outbound recipes, durable runs, lead management, automations, provider integrations, feedback, and runtime discovery. Use when an agent needs to install, authenticate, discover, or operate Coil through the same organization state humans use in the dashboard.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Coil API
|
|
7
|
+
|
|
8
|
+
Coil is an agent-operated outbound platform. Recipes are reusable workflows and runs are durable executions; prospect and Sales Navigator scrapes remain backing source/result records and compatibility commands.
|
|
9
|
+
|
|
10
|
+
The project principle is human-agent parity: when a feature exists in the UI, agents should have an API route, CLI command, and skill-level discoverability path for the same capability.
|
|
11
|
+
|
|
12
|
+
## First Command
|
|
13
|
+
|
|
14
|
+
Install the supported CLI if `coil` is not already available, then ask it what
|
|
15
|
+
it can do:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
if ! command -v coil >/dev/null 2>&1; then
|
|
19
|
+
npm install --global @usecoil/cli@0.1.0
|
|
20
|
+
fi
|
|
21
|
+
coil --version
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Start every new Coil runtime by asking the CLI what it can do:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
coil agent-context --json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For named environments:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
coil --profile prod agent-context --json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`agent-context` returns the selected profile, API base URL, auth mode, actor context, global flags, command manifests, output version information, and current caveats.
|
|
37
|
+
|
|
38
|
+
## Authentication
|
|
39
|
+
|
|
40
|
+
Prefer profile-based CLI auth:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
printf '%s' "$COIL_API_KEY" | coil auth login --profile prod --key -
|
|
44
|
+
coil auth use prod
|
|
45
|
+
coil --profile prod auth status --json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
On PowerShell:
|
|
49
|
+
|
|
50
|
+
```powershell
|
|
51
|
+
$env:COIL_API_KEY | coil auth login --profile prod --key -
|
|
52
|
+
coil auth use prod
|
|
53
|
+
coil --profile prod auth status --json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Coil API keys are first-party organization API keys. They start with `ak_` and are already scoped to an organization.
|
|
57
|
+
|
|
58
|
+
Do not configure `COIL_ORG_ID`. Coil does not need it for CLI/API calls.
|
|
59
|
+
|
|
60
|
+
Environment fallback is supported when a profile is not available:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export COIL_API_KEY="ak_..."
|
|
64
|
+
export COIL_BASE_URL="https://your-coil-host.example.com"
|
|
65
|
+
coil scrapes list --json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The CLI's built-in base URL is for local development. For the public service,
|
|
69
|
+
explicitly run `coil config set-base-url https://www.usecoil.com --profile prod`
|
|
70
|
+
before authenticating. Use `COIL_BASE_URL` only as an environment fallback when
|
|
71
|
+
you cannot persist a profile.
|
|
72
|
+
|
|
73
|
+
## Runtime setup
|
|
74
|
+
|
|
75
|
+
This skill is portable across Codex, Claude Code, Hermes Agent, and OpenClaw.
|
|
76
|
+
The runtime-specific installer or registry handles placement; Coil operations
|
|
77
|
+
always use the same public JSON CLI:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm install --global @usecoil/cli@0.1.0
|
|
81
|
+
coil config set-base-url https://www.usecoil.com --profile prod
|
|
82
|
+
printf '%s' "$COIL_API_KEY" | coil auth login --profile prod --key -
|
|
83
|
+
coil --profile prod agent-context --json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
On PowerShell, use `$env:COIL_API_KEY | coil auth login --profile prod --key -`
|
|
87
|
+
for the authentication step.
|
|
88
|
+
|
|
89
|
+
Supply `COIL_API_KEY` through the runtime secret manager and never place it in
|
|
90
|
+
command arguments. OpenClaw may provide it through the `coil-api` skill entry;
|
|
91
|
+
other runtimes may use their own secret manager. Do not add `COIL_ORG_ID`.
|
|
92
|
+
|
|
93
|
+
## CLI JSON Rules
|
|
94
|
+
|
|
95
|
+
- Use `--json` for machine-readable output.
|
|
96
|
+
- Use `--output-version 2` for standard list envelopes where supported.
|
|
97
|
+
- Version 2 list output shape is `{ resource, items, total, limit, offset, nextOffset, truncated, hint }`.
|
|
98
|
+
- Diagnostics and errors go to stderr.
|
|
99
|
+
- Global flags: `--profile`, `--base-url`, `--json`, `--output-version`.
|
|
100
|
+
|
|
101
|
+
## Common Workflows
|
|
102
|
+
|
|
103
|
+
### Orient
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
coil --profile prod agent-context --json
|
|
107
|
+
coil --profile prod config show --json
|
|
108
|
+
coil --profile prod auth status --json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Guided activation
|
|
112
|
+
|
|
113
|
+
Use the server-derived activation state before starting a new workspace flow. Provider-backed runs require a healthy saved connection and explicit spend confirmation.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
coil --profile prod activation status --json
|
|
117
|
+
coil --profile prod marketplace templates --json
|
|
118
|
+
coil --profile prod marketplace install coil.prospect-search --json
|
|
119
|
+
coil --profile prod recipes run <starter-recipe-id> --starter-run --confirm-provider-spend --new-scrape-name "Starter prospects" --titles "Founder" --locations "Singapore" --sizes "1-10" --fetch-count 25 --input '{"fetch_count":25}' --json
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The starter flow is capped at 25 leads. A queued or running response is not success; inspect or wait for the durable recipe run before claiming usable leads.
|
|
123
|
+
|
|
124
|
+
### Provider connections
|
|
125
|
+
|
|
126
|
+
Credential mutation and testing require a human organization admin. Keep secrets on stdin and never place them in arguments, logs, issue bodies, or recipe inputs.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
printf '%s' "$APIFY_API_TOKEN" | coil --profile prod integrations set apify --json
|
|
130
|
+
coil --profile prod integrations status apify --json
|
|
131
|
+
coil --profile prod integrations test apify --json
|
|
132
|
+
coil --profile prod integrations rotate --json
|
|
133
|
+
coil --profile prod integrations rotate --apply --json
|
|
134
|
+
coil --profile prod integrations disconnect apify --json
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Recipes and durable runs
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
coil --profile prod recipes list --status published --json
|
|
141
|
+
coil --profile prod recipes view <recipe-id> --json
|
|
142
|
+
coil --profile prod recipes validate <recipe-id> --json
|
|
143
|
+
coil --profile prod recipes run <recipe-id> --confirm-provider-spend --new-scrape-name "Q2 prospects" --input '{"fetch_count":100}' --json
|
|
144
|
+
coil --profile prod recipe-runs view <run-id> --json
|
|
145
|
+
coil --profile prod recipe-runs wait <run-id> --timeout 300 --interval 2 --json
|
|
146
|
+
coil --profile prod recipe-runs cancel <run-id> --json
|
|
147
|
+
coil --profile prod recipe-runs retry <run-id> --json
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Publication and ambiguous-effect reconciliation require a human organization admin. Retry/cancel behavior follows the durable run state returned by the API.
|
|
151
|
+
|
|
152
|
+
### Scrapes
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
coil --profile prod scrapes list --json --output-version 2
|
|
156
|
+
coil --profile prod scrapes get <scrape-id> --json
|
|
157
|
+
coil --profile prod scrapes create --type prospect --name "Q2 prospects" --titles "VP Sales,Head of Growth" --locations "United States" --sizes "11-50,51-200" --confirm-provider-spend --json
|
|
158
|
+
printf '%s' "$LINKEDIN_COOKIE" | coil --profile prod scrapes create --type sales-nav --name "Sales Nav export" --url "https://www.linkedin.com/sales/search/people?..." --user-agent "Mozilla/5.0 ..." --cookie-stdin --confirm-provider-spend --json
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Leads
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
coil --profile prod leads list --limit 50 --json --output-version 2
|
|
165
|
+
# Add a scrape ID after `list` to scope the result to one run.
|
|
166
|
+
coil --profile prod leads get <lead-id> --json
|
|
167
|
+
coil --profile prod leads export <scrape-id> --output leads.csv
|
|
168
|
+
coil --profile prod leads export <scrape-id> --view <view-id> --output saved-view.csv
|
|
169
|
+
coil --profile prod leads export <scrape-id> --ids <lead-id-1>,<lead-id-2> --output selected.csv
|
|
170
|
+
coil --profile prod --json leads export <scrape-id> --output leads.csv
|
|
171
|
+
coil --profile prod leads emails <scrape-id>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Automations
|
|
175
|
+
|
|
176
|
+
API-key agents can create draft automations. Human org admins publish drafts after validation.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
coil --profile prod automations create --name "Enrich leads" --webhook-url "https://hooks.example.com/enrich" --input-fields email,company_name --scope global --json
|
|
180
|
+
coil --profile prod automations list --status draft --json --output-version 2
|
|
181
|
+
coil --profile prod automations view <automation-id> --json
|
|
182
|
+
coil --profile prod automations validate <automation-id> --json
|
|
183
|
+
coil --profile prod automations results list --automation <automation-id> --json --output-version 2
|
|
184
|
+
coil --profile prod automations results links add <result-id> --kind webhook_evidence --url "https://logs.example.com/run/1" --label "n8n execution" --json
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Publishing, deletion, and egress policy management require a human org admin session:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
coil automations publish <automation-id> --json
|
|
191
|
+
coil automations policy get --json
|
|
192
|
+
coil automations policy set --domains hooks.example.com --fields email,company_name --json
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### SmartLead
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
coil --profile prod integrations status smartlead --json
|
|
199
|
+
coil --profile prod smartlead campaigns --json
|
|
200
|
+
coil --profile prod smartlead sequences --campaign <campaign-id> --json
|
|
201
|
+
coil --profile prod smartlead send <scrape-id> --campaign <campaign-id> --filter '[{"column":"email","operator":"is_not_empty","value":""}]' --json
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Feedback
|
|
205
|
+
|
|
206
|
+
Use feedback when a Coil task exposes product friction or a platform bug. Do not include secrets.
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
coil feedback "SmartLead send failed for selected leads" --type bug --json
|
|
210
|
+
coil feedback draft --type feature "Add export link metadata" --json
|
|
211
|
+
coil feedback drafts --json --output-version 2
|
|
212
|
+
coil feedback resend <feedback-id> --json
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## API Reference
|
|
216
|
+
|
|
217
|
+
Use the local reference files when you need raw HTTP details:
|
|
218
|
+
|
|
219
|
+
- `references/api-endpoints.md`
|
|
220
|
+
- `references/api-fields.md`
|
|
221
|
+
|
|
222
|
+
Prefer the CLI for routine operations because it handles auth, profiles, output normalization, and local retry behavior.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Coil API Endpoints
|
|
2
|
+
|
|
3
|
+
Base URL comes from the selected CLI profile or `COIL_BASE_URL`.
|
|
4
|
+
|
|
5
|
+
All authenticated requests use:
|
|
6
|
+
|
|
7
|
+
```http
|
|
8
|
+
Authorization: Bearer <session-token-or-ak-key>
|
|
9
|
+
Content-Type: application/json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Coil organization API keys (`ak_...`) are accepted by agent-compatible routes.
|
|
13
|
+
|
|
14
|
+
## Auth and Context
|
|
15
|
+
|
|
16
|
+
| Method | Path | Notes |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| GET | `/api/auth/status` | Returns authenticated actor, org, role, token type, and auth mode. |
|
|
19
|
+
| GET | `/api/auth/api-keys` | Lists Coil organization API keys for human org admins. |
|
|
20
|
+
| DELETE | `/api/auth/api-keys/{id}` | Revokes a Coil organization API key for human org admins. |
|
|
21
|
+
| POST | `/api/auth/verify-key` | Verifies a Coil organization API key without exposing it. |
|
|
22
|
+
|
|
23
|
+
CLI discovery wrapper:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
coil agent-context --json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Activation, Marketplace, and Recipes
|
|
30
|
+
|
|
31
|
+
| Method | Path | CLI |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| GET | `/api/activation` | `coil activation status --json` |
|
|
34
|
+
| GET | `/api/marketplace/templates` | `coil marketplace templates --json` |
|
|
35
|
+
| POST | `/api/marketplace/templates/{id}/install` | `coil marketplace install <template-id> --json` |
|
|
36
|
+
| GET | `/api/recipes` | `coil recipes list --json` |
|
|
37
|
+
| GET | `/api/recipes/{id}` | `coil recipes view <recipe-id> --json` |
|
|
38
|
+
| POST | `/api/recipes/{id}/validate` | `coil recipes validate <recipe-id> --json` |
|
|
39
|
+
| POST | `/api/recipes/{id}/publish` | `coil recipes publish <recipe-id> --json` |
|
|
40
|
+
| POST | `/api/recipes/{id}/run` | `coil recipes run <recipe-id> --confirm-provider-spend ... --json` |
|
|
41
|
+
| GET | `/api/recipe-runs/{id}` | `coil recipe-runs view <run-id> --json` |
|
|
42
|
+
| POST | `/api/recipe-runs/{id}/cancel` | `coil recipe-runs cancel <run-id> --json` |
|
|
43
|
+
| POST | `/api/recipe-runs/{id}/retry` | `coil recipe-runs retry <run-id> --json` |
|
|
44
|
+
|
|
45
|
+
Provider-backed admission requires explicit spend confirmation. A `202` queued response is durable admission, not terminal success. Recipe publication and ambiguous-effect reconciliation require a human organization admin.
|
|
46
|
+
|
|
47
|
+
## Scrapes
|
|
48
|
+
|
|
49
|
+
| Method | Path | CLI |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| GET | `/api/scrapes?limit=100` | `coil scrapes list --json` |
|
|
52
|
+
| POST | `/api/scrapes` | `coil scrapes create ... --json` |
|
|
53
|
+
| GET | `/api/scrapes/{id}` | `coil scrapes get <id> --json` |
|
|
54
|
+
| PATCH | `/api/scrapes/{id}` | `coil scrapes rename <id> --name ...` |
|
|
55
|
+
| DELETE | `/api/scrapes/{id}` | `coil scrapes delete <id> --force` |
|
|
56
|
+
| POST | `/api/scrapes/{id}/recount` | `coil scrapes recount <id> --json` |
|
|
57
|
+
| GET | `/api/scrapes/{id}/leads` | `coil leads list <scrape-id> --json` |
|
|
58
|
+
| GET | `/api/leads` | `coil leads list --json` |
|
|
59
|
+
|
|
60
|
+
Machine callers can create scrapes. Admin-gated destructive routes require a human org admin session.
|
|
61
|
+
|
|
62
|
+
## Leads
|
|
63
|
+
|
|
64
|
+
| Method | Path | CLI |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| GET | `/api/scrapes/{scrapeId}/leads?limit=50&offset=0` | `coil leads list <scrape-id> --json` |
|
|
67
|
+
| GET | `/api/leads?limit=50&offset=0` | `coil leads list --json` |
|
|
68
|
+
| GET | `/api/leads/{id}` | `coil leads get <id> --json` |
|
|
69
|
+
| PATCH | `/api/leads/{id}` | `coil leads update <id> --fields '{...}' --json` |
|
|
70
|
+
| DELETE | `/api/leads/{id}` | `coil leads delete <id> --force --json` |
|
|
71
|
+
| PATCH | `/api/leads/bulk` | `coil leads bulk-update --ids ... --fields '{...}' --json` |
|
|
72
|
+
| POST | `/api/export/csv` | `coil leads export <scrape-id> [--filter/--view/--ids] [--output]` |
|
|
73
|
+
| GET | `/api/leads/emails?scrapeId={id}` | `coil leads emails <scrape-id>` |
|
|
74
|
+
|
|
75
|
+
## Automations
|
|
76
|
+
|
|
77
|
+
| Method | Path | CLI |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| GET | `/api/automations?status=published` | `coil automations list --json` |
|
|
80
|
+
| POST | `/api/automations` | `coil automations create ... --json` |
|
|
81
|
+
| GET | `/api/automations/{id}` | `coil automations view <id> --json` |
|
|
82
|
+
| PATCH | `/api/automations/{id}` | `coil automations update <id> ... --json` |
|
|
83
|
+
| DELETE | `/api/automations/{id}` | `coil automations delete <id> --force --json` |
|
|
84
|
+
| POST | `/api/automations/{id}/validate` | `coil automations validate <id> --json` |
|
|
85
|
+
| POST | `/api/automations/{id}/publish` | `coil automations publish <id> --json` |
|
|
86
|
+
| POST | `/api/automations/{id}/run` | `coil automations run <id> --scrape <scrape-id> --json` |
|
|
87
|
+
| GET | `/api/automations/{id}/results` | `coil automations results list --automation <id> --json` |
|
|
88
|
+
| GET | `/api/automations/results` | `coil automations results list --json` |
|
|
89
|
+
| GET | `/api/automations/results/{resultId}` | `coil automations results view <result-id> --json` |
|
|
90
|
+
| GET | `/api/automations/results/{resultId}/links` | `coil automations results links list <result-id> --json` |
|
|
91
|
+
| POST | `/api/automations/results/{resultId}/links` | `coil automations results links add <result-id> --kind webhook_evidence --url https://... --json` |
|
|
92
|
+
| DELETE | `/api/automations/results/{resultId}/links/{linkId}` | `coil automations results links delete <result-id> <link-id> --json` |
|
|
93
|
+
| GET | `/api/automations/policy` | `coil automations policy get --json` |
|
|
94
|
+
| PUT | `/api/automations/policy` | `coil automations policy set --domains ... --fields ... --json` |
|
|
95
|
+
|
|
96
|
+
API-key callers create draft automations. Publishing, deletion, and policy changes require a human org admin session. Automation result links are deliberately narrow traceability records for delivery evidence; they are not a generic metadata surface.
|
|
97
|
+
|
|
98
|
+
## Settings, Preferences, Members
|
|
99
|
+
|
|
100
|
+
| Method | Path | CLI |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| GET | `/api/settings` | `coil settings get --json` |
|
|
103
|
+
| PATCH | `/api/settings` | `coil settings set <key> <value> --json` |
|
|
104
|
+
| GET | `/api/preferences/columns?scrapeId={id}` | `coil preferences get --scrape <id> --json` |
|
|
105
|
+
| PUT | `/api/preferences/columns` | `coil preferences set --scrape <id> ... --json` |
|
|
106
|
+
| GET | `/api/members` | `coil members list --json` |
|
|
107
|
+
|
|
108
|
+
## Integrations and SmartLead
|
|
109
|
+
|
|
110
|
+
| Method | Path | CLI |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| GET | `/api/integrations` | `coil integrations list --json` |
|
|
113
|
+
| GET | `/api/integrations/{provider}` | `coil integrations status <provider> --json` |
|
|
114
|
+
| PUT | `/api/integrations/{provider}` | `printf '%s' "$PROVIDER_API_KEY" \| coil integrations set <provider> --json` |
|
|
115
|
+
| DELETE | `/api/integrations/{provider}` | `coil integrations disconnect <provider> --json` |
|
|
116
|
+
| POST | `/api/integrations/{provider}/test` | `coil integrations test <provider> --json` |
|
|
117
|
+
| POST | `/api/integrations/rotate` | `coil integrations rotate [--apply] --json` |
|
|
118
|
+
| GET | `/api/integrations/smartlead/campaigns` | `coil smartlead campaigns --json` |
|
|
119
|
+
| GET | `/api/integrations/smartlead/campaigns/sequences?campaignId={id}` | `coil smartlead sequences --campaign <id> --json` |
|
|
120
|
+
| POST | `/api/integrations/smartlead/send` | `coil smartlead send <scrape-id> --campaign <id> --json` |
|
|
121
|
+
|
|
122
|
+
## Feedback
|
|
123
|
+
|
|
124
|
+
| Method | Path | CLI |
|
|
125
|
+
| --- | --- | --- |
|
|
126
|
+
| POST | `/api/feedback` | `coil feedback "..." --json` |
|
|
127
|
+
|
|
128
|
+
The CLI also supports local feedback drafts and resend commands for retryable failures.
|
|
129
|
+
|
|
130
|
+
## Activity and Usage
|
|
131
|
+
|
|
132
|
+
| Method | Path | CLI |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| GET | `/api/activity` | `coil activity list --json` |
|
|
135
|
+
| GET | `/api/usage?view=summary` | `coil usage summary --json` |
|
|
136
|
+
| GET | `/api/usage?view=events` | `coil usage events --json` |
|
|
137
|
+
| GET | `/api/usage?view=limits` | `coil usage limits --json` |
|
|
138
|
+
|
|
139
|
+
Activity cursors are opaque. Usage endpoints require an organization admin, so machine API keys are intentionally rejected by the shared role gate.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Coil API Fields
|
|
2
|
+
|
|
3
|
+
Responses use snake_case. Request bodies generally accept the field names shown by the CLI and API route schemas.
|
|
4
|
+
|
|
5
|
+
## Auth Context
|
|
6
|
+
|
|
7
|
+
`GET /api/auth/status` returns:
|
|
8
|
+
|
|
9
|
+
| Field | Type | Notes |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `authenticated` | boolean | Whether the bearer token is valid. |
|
|
12
|
+
| `userId` | string/null | Human Clerk user ID when present. |
|
|
13
|
+
| `orgId` | string/null | Clerk organization ID. |
|
|
14
|
+
| `orgRole` | `org:admin`/`org:member`/null | Role for human sessions; API keys map to member behavior. |
|
|
15
|
+
| `tokenType` | string/null | Usually `session_token` or `api_key`. |
|
|
16
|
+
| `actor` | object | `{ type, id, name }` where type is `human` or `machine`. |
|
|
17
|
+
|
|
18
|
+
## CLI Output Envelope
|
|
19
|
+
|
|
20
|
+
Use `--json --output-version 2` on list commands when possible:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"resource": "scrapes",
|
|
25
|
+
"items": [],
|
|
26
|
+
"total": 0,
|
|
27
|
+
"limit": 25,
|
|
28
|
+
"offset": 0,
|
|
29
|
+
"nextOffset": null,
|
|
30
|
+
"truncated": false,
|
|
31
|
+
"hint": null
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Scrape
|
|
36
|
+
|
|
37
|
+
| Field | Type | Notes |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `id` | string | UUID. |
|
|
40
|
+
| `org_id` | string | Tenant isolation key. |
|
|
41
|
+
| `name` | string | Human-visible scrape name. |
|
|
42
|
+
| `status` | string | `pending`, `running`, `completed`, `failed`, or legacy/null values. |
|
|
43
|
+
| `job_titles` | string[] | Prospect scrape filter. |
|
|
44
|
+
| `locations` | string[] | Prospect scrape filter. |
|
|
45
|
+
| `company_sizes` | string[] | Prospect scrape filter. |
|
|
46
|
+
| `industries_include` | string[]/null | Prospect filter. |
|
|
47
|
+
| `industries_exclude` | string[]/null | Prospect filter. |
|
|
48
|
+
| `search_url` | string/null | Sales Navigator source URL in create payloads. |
|
|
49
|
+
| `fetch_count` | number | Requested prospect count. |
|
|
50
|
+
| `prospect_count` | number | Current stored lead count. |
|
|
51
|
+
| `smartlead_campaign_id` | string/null | Optional auto-send campaign. |
|
|
52
|
+
| `created_by_user_id` | string/null | Human creator when known. |
|
|
53
|
+
| `created_at` | ISO string | Creation timestamp. |
|
|
54
|
+
|
|
55
|
+
Create payloads:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"type": "prospect",
|
|
60
|
+
"name": "Q2 prospects",
|
|
61
|
+
"job_titles": ["VP Sales"],
|
|
62
|
+
"locations": ["United States"],
|
|
63
|
+
"company_sizes": ["11-50"],
|
|
64
|
+
"fetch_count": 100,
|
|
65
|
+
"confirm_provider_spend": true
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"type": "sales-nav",
|
|
72
|
+
"name": "Sales Nav export",
|
|
73
|
+
"search_url": "https://www.linkedin.com/sales/search/people?...",
|
|
74
|
+
"user_agent": "Mozilla/5.0 ...",
|
|
75
|
+
"linkedin_cookie": "li_at=...",
|
|
76
|
+
"confirm_provider_spend": true
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`confirm_provider_spend` is required for provider-backed acquisition. Sales Navigator credentials are resolved server-side for execution and must not be stored in recipe inputs or copied into logs.
|
|
81
|
+
|
|
82
|
+
## Lead
|
|
83
|
+
|
|
84
|
+
| Field | Type | Notes |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| `id` | string | UUID. |
|
|
87
|
+
| `org_id` | string | Tenant isolation key. |
|
|
88
|
+
| `scrape_id` | string | Parent scrape. |
|
|
89
|
+
| `full_name` | string/null | Lead name. |
|
|
90
|
+
| `first_name` | string/null | First name. |
|
|
91
|
+
| `last_name` | string/null | Last name. |
|
|
92
|
+
| `email` | string/null | Email when known. |
|
|
93
|
+
| `linkedin` | string/null | LinkedIn profile URL. |
|
|
94
|
+
| `job_title` | string/null | Role/title. |
|
|
95
|
+
| `company_name` | string/null | Company. |
|
|
96
|
+
| `company_domain` | string/null | Company domain. |
|
|
97
|
+
| `location` | string/null | Lead location. |
|
|
98
|
+
| `is_bad_fit` | boolean/null | Qualification marker. |
|
|
99
|
+
| `added_to_smartlead` | boolean/null | SmartLead send marker. |
|
|
100
|
+
| `smartlead_campaign` | string/null | Campaign name. |
|
|
101
|
+
| `date_scraped` | string/null | Scrape date. |
|
|
102
|
+
|
|
103
|
+
Filter conditions use:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"column": "email",
|
|
108
|
+
"operator": "is_not_empty",
|
|
109
|
+
"value": ""
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Supported operators include equality, contains, empty/not-empty, booleans, and date comparisons. Use `coil agent-context --json` for the current command manifest.
|
|
114
|
+
|
|
115
|
+
## Automation
|
|
116
|
+
|
|
117
|
+
| Field | Type | Notes |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| `id` | string | UUID. |
|
|
120
|
+
| `org_id` | string | Tenant isolation key. |
|
|
121
|
+
| `name` | string | Automation name. |
|
|
122
|
+
| `webhook_url` | string | HTTPS webhook URL. |
|
|
123
|
+
| `input_fields` | string[] | Lead fields sent to the webhook. |
|
|
124
|
+
| `scope` | `global`/`scrape` | Whether automation applies globally or to one scrape. |
|
|
125
|
+
| `scrape_id` | string/null | Required for scrape-scoped automation. |
|
|
126
|
+
| `status` | `draft`/`published`/`disabled` | API-key agents create drafts. |
|
|
127
|
+
| `created_by_actor_type` | `human`/`machine` | Actor attribution. |
|
|
128
|
+
| `created_by_actor_id` | string | User ID or API key ID. |
|
|
129
|
+
| `created_by_actor_name` | string/null | API key display name when available. |
|
|
130
|
+
| `published_by_user_id` | string/null | Human admin publisher. |
|
|
131
|
+
| `published_at` | ISO string/null | Publication timestamp. |
|
|
132
|
+
|
|
133
|
+
Create payload:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"name": "Enrich leads",
|
|
138
|
+
"webhook_url": "https://hooks.example.com/enrich",
|
|
139
|
+
"input_fields": ["email", "company_name"],
|
|
140
|
+
"scope": "global",
|
|
141
|
+
"scrape_id": null
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Automation Result
|
|
146
|
+
|
|
147
|
+
| Field | Type | Notes |
|
|
148
|
+
| --- | --- | --- |
|
|
149
|
+
| `id` | string | UUID. |
|
|
150
|
+
| `automation_id` | string | Parent automation. |
|
|
151
|
+
| `lead_id` | string | Processed lead. |
|
|
152
|
+
| `status` | `pending`/`running`/`success`/`error` | Delivery state. |
|
|
153
|
+
| `result` | string/null | Webhook response or summary. |
|
|
154
|
+
| `error_message` | string/null | Failure detail. |
|
|
155
|
+
| `created_at` | ISO string | Creation timestamp. |
|
|
156
|
+
| `updated_at` | ISO string | Last status update. |
|
|
157
|
+
|
|
158
|
+
## Automation Result Link
|
|
159
|
+
|
|
160
|
+
| Field | Type | Notes |
|
|
161
|
+
| --- | --- | --- |
|
|
162
|
+
| `id` | string | UUID. |
|
|
163
|
+
| `automation_result_id` | string | Parent automation result. |
|
|
164
|
+
| `kind` | `webhook_evidence`/`smartlead_campaign`/`delivery_evidence` | Accepted evidence class. |
|
|
165
|
+
| `label` | string/null | Short label or delivery ID, max 120 characters. |
|
|
166
|
+
| `url` | string | HTTPS URL, max 2048 characters. `smartlead_campaign` URLs must use a `smartlead.ai` host. |
|
|
167
|
+
| `created_at` | ISO string | Creation timestamp. |
|
|
168
|
+
|
|
169
|
+
Create payload:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"kind": "webhook_evidence",
|
|
174
|
+
"label": "n8n execution",
|
|
175
|
+
"url": "https://logs.example.com/run/1"
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Automation Policy
|
|
180
|
+
|
|
181
|
+
| Field | Type | Notes |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| `domains` | string[] | Exact or wildcard allowed webhook hosts. |
|
|
184
|
+
| `fields` | string[] | Allowed lead fields for automation payloads. |
|
|
185
|
+
|
|
186
|
+
Only human org admins can update policy.
|
|
187
|
+
|
|
188
|
+
## Settings
|
|
189
|
+
|
|
190
|
+
Settings are stored in org-scoped JSON. Common keys:
|
|
191
|
+
|
|
192
|
+
| Key | Type | Notes |
|
|
193
|
+
| --- | --- | --- |
|
|
194
|
+
| `has_smartlead_key` | boolean | Read-only redacted presence marker. |
|
|
195
|
+
| `smartlead_api_key` | string | Legacy setting. New writes are rejected; configure SmartLead with `printf '%s' "$SMARTLEAD_API_KEY" \| coil integrations set smartlead` or `PUT /api/integrations/smartlead`. |
|
|
196
|
+
| `default_columns` | string[] | Optional lead table defaults. |
|
|
197
|
+
|
|
198
|
+
## Agent Caveats
|
|
199
|
+
|
|
200
|
+
- API-key agents currently behave as org members, not org admins.
|
|
201
|
+
- Admin-gated actions include destructive scrape/automation operations, automation policy management, and automation publishing.
|
|
202
|
+
- Prefer `coil feedback` for platform friction discovered while operating Coil.
|