@caik.dev/cli 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/dist/index.js +816 -0
- package/package.json +27 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
8
|
+
import chalk2 from "chalk";
|
|
9
|
+
|
|
10
|
+
// src/errors.ts
|
|
11
|
+
var CaikError = class extends Error {
|
|
12
|
+
constructor(message, suggestion, exitCode = 1) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.suggestion = suggestion;
|
|
15
|
+
this.exitCode = exitCode;
|
|
16
|
+
this.name = "CaikError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
function mapHttpError(status, body) {
|
|
20
|
+
const message = typeof body === "object" && body !== null && "message" in body ? String(body.message) : `Request failed with status ${status}`;
|
|
21
|
+
switch (status) {
|
|
22
|
+
case 401:
|
|
23
|
+
return new CaikError(message, "Run 'caik init --auth' to sign in.");
|
|
24
|
+
case 403:
|
|
25
|
+
return new CaikError(message, "You don't have permission for this action.");
|
|
26
|
+
case 404:
|
|
27
|
+
return new CaikError(message, "Run 'caik search <query>' to find artifacts.");
|
|
28
|
+
case 400:
|
|
29
|
+
return new CaikError(message, "Check your input and try again.");
|
|
30
|
+
case 429:
|
|
31
|
+
return new CaikError("Rate limited. Please wait and try again.");
|
|
32
|
+
case 500:
|
|
33
|
+
case 502:
|
|
34
|
+
case 503:
|
|
35
|
+
return new CaikError("Server error. The API may be temporarily unavailable.", "Try again in a few moments, or check 'caik status'.");
|
|
36
|
+
default:
|
|
37
|
+
return new CaikError(message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function mapNetworkError(err) {
|
|
41
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
42
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
43
|
+
return new CaikError(
|
|
44
|
+
"Could not connect to the CAIK API.",
|
|
45
|
+
"Check your --api-url or CAIK_API_URL environment variable. Is the server running?"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (msg.includes("ETIMEDOUT") || msg.includes("timeout")) {
|
|
49
|
+
return new CaikError(
|
|
50
|
+
"Request timed out.",
|
|
51
|
+
"The API may be slow or unreachable. Try again or check 'caik status'."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return new CaikError(`Network error: ${msg}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/api.ts
|
|
58
|
+
var CaikApiClient = class {
|
|
59
|
+
baseUrl;
|
|
60
|
+
apiKey;
|
|
61
|
+
verbose;
|
|
62
|
+
constructor(config) {
|
|
63
|
+
this.baseUrl = config.apiUrl.replace(/\/+$/, "");
|
|
64
|
+
this.apiKey = config.apiKey;
|
|
65
|
+
this.verbose = config.verbose ?? false;
|
|
66
|
+
}
|
|
67
|
+
async get(path, params) {
|
|
68
|
+
const url = new URL(`${this.baseUrl}/api/v1${path}`);
|
|
69
|
+
if (params) {
|
|
70
|
+
for (const [key, value] of Object.entries(params)) {
|
|
71
|
+
if (value !== void 0) {
|
|
72
|
+
url.searchParams.set(key, String(value));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return this.request(url.toString(), { method: "GET" });
|
|
77
|
+
}
|
|
78
|
+
async post(path, body) {
|
|
79
|
+
return this.request(`${this.baseUrl}/api/v1${path}`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "Content-Type": "application/json" },
|
|
82
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async patch(path, body) {
|
|
86
|
+
return this.request(`${this.baseUrl}/api/v1${path}`, {
|
|
87
|
+
method: "PATCH",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async del(path) {
|
|
93
|
+
return this.request(`${this.baseUrl}/api/v1${path}`, { method: "DELETE" });
|
|
94
|
+
}
|
|
95
|
+
async request(url, init) {
|
|
96
|
+
const headers = {
|
|
97
|
+
...init.headers
|
|
98
|
+
};
|
|
99
|
+
if (this.apiKey) {
|
|
100
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
101
|
+
}
|
|
102
|
+
if (this.verbose) {
|
|
103
|
+
console.error(`[verbose] ${init.method} ${url}`);
|
|
104
|
+
}
|
|
105
|
+
const start = Date.now();
|
|
106
|
+
let response;
|
|
107
|
+
try {
|
|
108
|
+
response = await fetch(url, { ...init, headers });
|
|
109
|
+
} catch (err) {
|
|
110
|
+
throw mapNetworkError(err);
|
|
111
|
+
}
|
|
112
|
+
if (this.verbose) {
|
|
113
|
+
console.error(`[verbose] ${response.status} (${Date.now() - start}ms)`);
|
|
114
|
+
}
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
let body;
|
|
117
|
+
try {
|
|
118
|
+
body = await response.json();
|
|
119
|
+
} catch {
|
|
120
|
+
body = await response.text().catch(() => null);
|
|
121
|
+
}
|
|
122
|
+
throw mapHttpError(response.status, body);
|
|
123
|
+
}
|
|
124
|
+
if (response.status === 204) {
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
return response.json();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// src/config.ts
|
|
132
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from "fs";
|
|
133
|
+
import { join } from "path";
|
|
134
|
+
import { homedir } from "os";
|
|
135
|
+
var DEFAULT_CONFIG = {
|
|
136
|
+
apiUrl: "https://caik.dev",
|
|
137
|
+
defaultLimit: 10,
|
|
138
|
+
version: 1
|
|
139
|
+
};
|
|
140
|
+
function getConfigDir() {
|
|
141
|
+
return join(homedir(), ".caik");
|
|
142
|
+
}
|
|
143
|
+
function getConfigPath() {
|
|
144
|
+
return join(getConfigDir(), "config.json");
|
|
145
|
+
}
|
|
146
|
+
function readConfig() {
|
|
147
|
+
const path = getConfigPath();
|
|
148
|
+
if (!existsSync(path)) {
|
|
149
|
+
return { ...DEFAULT_CONFIG };
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const raw = readFileSync(path, "utf-8");
|
|
153
|
+
const parsed = JSON.parse(raw);
|
|
154
|
+
return { ...DEFAULT_CONFIG, ...parsed };
|
|
155
|
+
} catch {
|
|
156
|
+
return { ...DEFAULT_CONFIG };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function writeConfig(config) {
|
|
160
|
+
const dir = getConfigDir();
|
|
161
|
+
if (!existsSync(dir)) {
|
|
162
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
163
|
+
}
|
|
164
|
+
const path = getConfigPath();
|
|
165
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
166
|
+
chmodSync(path, 384);
|
|
167
|
+
}
|
|
168
|
+
function resolveConfig(opts) {
|
|
169
|
+
const config = readConfig();
|
|
170
|
+
return {
|
|
171
|
+
apiUrl: opts.apiUrl ?? process.env.CAIK_API_URL ?? config.apiUrl ?? DEFAULT_CONFIG.apiUrl,
|
|
172
|
+
apiKey: opts.apiKey ?? process.env.CAIK_API_KEY ?? config.apiKey
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/format.ts
|
|
177
|
+
import chalk from "chalk";
|
|
178
|
+
import ora from "ora";
|
|
179
|
+
import Table from "cli-table3";
|
|
180
|
+
function success(msg) {
|
|
181
|
+
return chalk.green(`\u2713 ${msg}`);
|
|
182
|
+
}
|
|
183
|
+
function error(msg) {
|
|
184
|
+
return chalk.red(`\u2717 ${msg}`);
|
|
185
|
+
}
|
|
186
|
+
function info(msg) {
|
|
187
|
+
return chalk.cyan(`\u2192 ${msg}`);
|
|
188
|
+
}
|
|
189
|
+
function dim(msg) {
|
|
190
|
+
return chalk.dim(msg);
|
|
191
|
+
}
|
|
192
|
+
function heading(msg) {
|
|
193
|
+
return chalk.bold.white(msg);
|
|
194
|
+
}
|
|
195
|
+
function createSpinner(text) {
|
|
196
|
+
return ora({ text, color: "cyan" });
|
|
197
|
+
}
|
|
198
|
+
function renderTable(headers, rows) {
|
|
199
|
+
const table = new Table({
|
|
200
|
+
head: headers.map((h) => chalk.bold.cyan(h)),
|
|
201
|
+
style: { head: [], border: ["dim"] },
|
|
202
|
+
chars: {
|
|
203
|
+
top: "\u2500",
|
|
204
|
+
"top-mid": "\u252C",
|
|
205
|
+
"top-left": "\u250C",
|
|
206
|
+
"top-right": "\u2510",
|
|
207
|
+
bottom: "\u2500",
|
|
208
|
+
"bottom-mid": "\u2534",
|
|
209
|
+
"bottom-left": "\u2514",
|
|
210
|
+
"bottom-right": "\u2518",
|
|
211
|
+
left: "\u2502",
|
|
212
|
+
"left-mid": "\u251C",
|
|
213
|
+
mid: "\u2500",
|
|
214
|
+
"mid-mid": "\u253C",
|
|
215
|
+
right: "\u2502",
|
|
216
|
+
"right-mid": "\u2524",
|
|
217
|
+
middle: "\u2502"
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
for (const row of rows) {
|
|
221
|
+
table.push(row);
|
|
222
|
+
}
|
|
223
|
+
return table.toString();
|
|
224
|
+
}
|
|
225
|
+
function outputResult(data, opts) {
|
|
226
|
+
if (opts.json) {
|
|
227
|
+
console.log(JSON.stringify(data, null, 2));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function formatNumber(n) {
|
|
231
|
+
return n.toLocaleString("en-US");
|
|
232
|
+
}
|
|
233
|
+
function truncate(str, maxLen) {
|
|
234
|
+
if (str.length <= maxLen) return str;
|
|
235
|
+
return str.slice(0, maxLen - 1) + "\u2026";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/commands/search.ts
|
|
239
|
+
function registerSearchCommand(program2) {
|
|
240
|
+
program2.command("search <query>").description("Search for artifacts in the CAIK registry").option("--primitive <type>", "Filter by primitive type (executable, knowledge, connector, composition, reference)").option("--tag <tag>", "Filter by tag (can be repeated)", (val, prev) => [...prev, val], []).option("--platform <platform>", "Filter by platform (claude, cursor, node, etc.)").option("--limit <n>", "Maximum results to return", "10").option("--offset <n>", "Skip first N results (for pagination)", "0").addHelpText("after", `
|
|
241
|
+
Examples:
|
|
242
|
+
caik search "auth middleware"
|
|
243
|
+
caik search "calendar" --primitive executable --limit 5
|
|
244
|
+
caik search "react" --tag frontend --json`).action(async (query, opts) => {
|
|
245
|
+
const globalOpts = program2.opts();
|
|
246
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
247
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
248
|
+
const spinner = createSpinner("Searching...");
|
|
249
|
+
if (!globalOpts.json) spinner.start();
|
|
250
|
+
const filters = {};
|
|
251
|
+
if (opts.primitive) filters.primitive = opts.primitive;
|
|
252
|
+
if (opts.tag.length > 0) filters.tags = opts.tag;
|
|
253
|
+
if (opts.platform) filters.platforms = [opts.platform];
|
|
254
|
+
const result = await client.post("/search", {
|
|
255
|
+
query,
|
|
256
|
+
filters: Object.keys(filters).length > 0 ? filters : void 0,
|
|
257
|
+
limit: parseInt(opts.limit, 10),
|
|
258
|
+
offset: parseInt(opts.offset, 10)
|
|
259
|
+
});
|
|
260
|
+
if (!globalOpts.json) spinner.stop();
|
|
261
|
+
if (globalOpts.json) {
|
|
262
|
+
outputResult(result, { json: true });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (result.results.length === 0) {
|
|
266
|
+
console.log(`No artifacts found for "${query}".`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const table = renderTable(
|
|
270
|
+
["Name", "Primitive", "Quality", "Installs", "Tags"],
|
|
271
|
+
result.results.map((a) => [
|
|
272
|
+
a.slug,
|
|
273
|
+
a.primitive,
|
|
274
|
+
a.qualityScore != null ? String(a.qualityScore) : "\u2014",
|
|
275
|
+
formatNumber(a.installCount),
|
|
276
|
+
truncate((a.tags ?? []).join(", "), 30)
|
|
277
|
+
])
|
|
278
|
+
);
|
|
279
|
+
console.log(table);
|
|
280
|
+
console.log(`
|
|
281
|
+
${result.total} result${result.total !== 1 ? "s" : ""} found.`);
|
|
282
|
+
if (result.hasMore) {
|
|
283
|
+
console.log(`Use --offset ${parseInt(opts.offset, 10) + parseInt(opts.limit, 10)} to see more.`);
|
|
284
|
+
}
|
|
285
|
+
client.post("/telemetry/search", {
|
|
286
|
+
query,
|
|
287
|
+
resultsCount: result.total
|
|
288
|
+
}).catch(() => {
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/commands/install.ts
|
|
294
|
+
import { existsSync as existsSync2 } from "fs";
|
|
295
|
+
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
296
|
+
import { dirname, resolve } from "path";
|
|
297
|
+
import { execSync } from "child_process";
|
|
298
|
+
import { createInterface } from "readline/promises";
|
|
299
|
+
function detectPlatform() {
|
|
300
|
+
if (existsSync2(".claude") || existsSync2("CLAUDE.md")) return "claude";
|
|
301
|
+
if (existsSync2(".cursor")) return "cursor";
|
|
302
|
+
if (existsSync2("node_modules") || existsSync2("package.json")) return "node";
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
function registerInstallCommand(program2) {
|
|
306
|
+
program2.command("install <slug>").description("Install an artifact from the CAIK registry").option("--platform <platform>", "Target platform (auto-detected if not specified)").option("-y, --yes", "Skip confirmation prompts (for CI/scripting)").addHelpText("after", `
|
|
307
|
+
Examples:
|
|
308
|
+
caik install auth-middleware
|
|
309
|
+
caik install my-skill --platform claude`).action(async (slug, opts) => {
|
|
310
|
+
const globalOpts = program2.opts();
|
|
311
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
312
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
313
|
+
const platform = opts.platform ?? detectPlatform();
|
|
314
|
+
const spinner = createSpinner(`Fetching ${slug}...`);
|
|
315
|
+
if (!globalOpts.json) spinner.start();
|
|
316
|
+
const params = { platform };
|
|
317
|
+
const installInfo = await client.get(`/install/${encodeURIComponent(slug)}`, params);
|
|
318
|
+
if (globalOpts.json) {
|
|
319
|
+
spinner.stop();
|
|
320
|
+
outputResult(installInfo, { json: true });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
spinner.text = `Installing ${installInfo.artifact.name}...`;
|
|
324
|
+
const skipConfirm = Boolean(opts.yes);
|
|
325
|
+
const cwd = process.cwd();
|
|
326
|
+
const safeFiles = [];
|
|
327
|
+
if (installInfo.files && installInfo.files.length > 0) {
|
|
328
|
+
for (const file of installInfo.files) {
|
|
329
|
+
const resolvedPath = resolve(cwd, file.path);
|
|
330
|
+
if (!resolvedPath.startsWith(cwd + "/") && resolvedPath !== cwd) {
|
|
331
|
+
spinner.stop();
|
|
332
|
+
console.log(error(`Rejected file path outside working directory: ${file.path}`));
|
|
333
|
+
spinner.start();
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
safeFiles.push({ resolvedPath, content: file.content });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const hasFiles = safeFiles.length > 0;
|
|
340
|
+
const hasInstallCmd = Boolean(installInfo.installCommand);
|
|
341
|
+
const hasPostHook = Boolean(installInfo.postInstallHook);
|
|
342
|
+
if (!skipConfirm && (hasFiles || hasInstallCmd || hasPostHook)) {
|
|
343
|
+
spinner.stop();
|
|
344
|
+
console.log(info("\n--- Install plan ---"));
|
|
345
|
+
if (hasFiles) {
|
|
346
|
+
console.log(info("Files to write:"));
|
|
347
|
+
for (const f of safeFiles) {
|
|
348
|
+
console.log(info(` ${f.resolvedPath}`));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (hasInstallCmd) {
|
|
352
|
+
console.log(info(`Install command: ${installInfo.installCommand}`));
|
|
353
|
+
}
|
|
354
|
+
if (hasPostHook) {
|
|
355
|
+
console.log(info(`Post-install hook: ${installInfo.postInstallHook}`));
|
|
356
|
+
}
|
|
357
|
+
console.log();
|
|
358
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
359
|
+
const answer = await rl.question("Continue? (y/N) ");
|
|
360
|
+
rl.close();
|
|
361
|
+
if (answer.toLowerCase() !== "y") {
|
|
362
|
+
console.log(info("Installation cancelled."));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
spinner.start();
|
|
366
|
+
}
|
|
367
|
+
for (const file of safeFiles) {
|
|
368
|
+
const dir = dirname(file.resolvedPath);
|
|
369
|
+
if (!existsSync2(dir)) {
|
|
370
|
+
mkdirSync2(dir, { recursive: true });
|
|
371
|
+
}
|
|
372
|
+
writeFileSync2(file.resolvedPath, file.content, "utf-8");
|
|
373
|
+
}
|
|
374
|
+
if (installInfo.installCommand) {
|
|
375
|
+
try {
|
|
376
|
+
execSync(installInfo.installCommand, { stdio: "pipe" });
|
|
377
|
+
} catch (err) {
|
|
378
|
+
spinner.stop();
|
|
379
|
+
console.error(error(`Install command failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
380
|
+
client.post("/telemetry/install", {
|
|
381
|
+
artifactSlug: slug,
|
|
382
|
+
platform,
|
|
383
|
+
success: false,
|
|
384
|
+
errorMessage: err instanceof Error ? err.message : String(err)
|
|
385
|
+
}).catch(() => {
|
|
386
|
+
});
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (installInfo.postInstallHook) {
|
|
391
|
+
try {
|
|
392
|
+
execSync(installInfo.postInstallHook, { stdio: "pipe" });
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
spinner.stop();
|
|
397
|
+
console.log(success(`Installed ${installInfo.artifact.name}`));
|
|
398
|
+
if (installInfo.contract?.touchedPaths) {
|
|
399
|
+
console.log(info(`Files: ${installInfo.contract.touchedPaths.join(", ")}`));
|
|
400
|
+
}
|
|
401
|
+
client.post("/telemetry/install", {
|
|
402
|
+
artifactSlug: slug,
|
|
403
|
+
platform,
|
|
404
|
+
success: true
|
|
405
|
+
}).catch(() => {
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/commands/init.ts
|
|
411
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
412
|
+
import { stdin, stdout } from "process";
|
|
413
|
+
function registerInitCommand(program2) {
|
|
414
|
+
program2.command("init").description("Configure the CAIK CLI").option("--auth", "Set up authentication (opens browser)").addHelpText("after", `
|
|
415
|
+
Examples:
|
|
416
|
+
caik init
|
|
417
|
+
caik init --auth`).action(async (opts) => {
|
|
418
|
+
const rl = createInterface2({ input: stdin, output: stdout });
|
|
419
|
+
try {
|
|
420
|
+
const config = readConfig();
|
|
421
|
+
const apiUrl = await rl.question(`API URL (${config.apiUrl}): `);
|
|
422
|
+
if (apiUrl.trim()) {
|
|
423
|
+
config.apiUrl = apiUrl.trim();
|
|
424
|
+
}
|
|
425
|
+
const apiKey = await rl.question("API Key (leave blank to skip): ");
|
|
426
|
+
if (apiKey.trim()) {
|
|
427
|
+
config.apiKey = apiKey.trim();
|
|
428
|
+
}
|
|
429
|
+
writeConfig(config);
|
|
430
|
+
console.log(success("CAIK CLI configured"));
|
|
431
|
+
console.log(info(`Config saved to ~/.caik/config.json`));
|
|
432
|
+
if (config.apiKey) {
|
|
433
|
+
const client = new CaikApiClient({ apiUrl: config.apiUrl, apiKey: config.apiKey });
|
|
434
|
+
try {
|
|
435
|
+
const karma = await client.get("/me/karma");
|
|
436
|
+
console.log(success("API connection verified \u2014 authenticated"));
|
|
437
|
+
} catch {
|
|
438
|
+
console.log(error("Could not verify API connection. Check your API key."));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} finally {
|
|
442
|
+
rl.close();
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/commands/status.ts
|
|
448
|
+
import { existsSync as existsSync3 } from "fs";
|
|
449
|
+
function registerStatusCommand(program2) {
|
|
450
|
+
program2.command("status").description("Show CLI configuration and connectivity status").action(async () => {
|
|
451
|
+
const globalOpts = program2.opts();
|
|
452
|
+
const config = readConfig();
|
|
453
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
454
|
+
if (globalOpts.json) {
|
|
455
|
+
const data = {
|
|
456
|
+
apiUrl,
|
|
457
|
+
apiKeyConfigured: !!apiKey,
|
|
458
|
+
platforms: {
|
|
459
|
+
claude: existsSync3(".claude") || existsSync3("CLAUDE.md"),
|
|
460
|
+
cursor: existsSync3(".cursor"),
|
|
461
|
+
node: existsSync3("node_modules") || existsSync3("package.json")
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
const client2 = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
465
|
+
try {
|
|
466
|
+
if (apiKey) {
|
|
467
|
+
const karma = await client2.get("/me/karma");
|
|
468
|
+
data.authenticated = true;
|
|
469
|
+
data.user = karma;
|
|
470
|
+
}
|
|
471
|
+
data.apiReachable = true;
|
|
472
|
+
} catch {
|
|
473
|
+
data.apiReachable = false;
|
|
474
|
+
data.authenticated = false;
|
|
475
|
+
}
|
|
476
|
+
outputResult(data, { json: true });
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
console.log(heading("CAIK CLI Status"));
|
|
480
|
+
console.log("\u2550".repeat(40));
|
|
481
|
+
console.log(`API URL: ${apiUrl}`);
|
|
482
|
+
console.log(`API Key: ${apiKey ? `${dim("configured")} (${apiKey.slice(0, 12)}...)` : dim("not set")}`);
|
|
483
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
484
|
+
try {
|
|
485
|
+
if (apiKey) {
|
|
486
|
+
const karma = await client.get("/me/karma");
|
|
487
|
+
console.log(`Authenticated: ${success("yes")}`);
|
|
488
|
+
console.log(`User: ${karma.handle ?? karma.displayName ?? "unknown"} (${karma.karmaTier})`);
|
|
489
|
+
} else {
|
|
490
|
+
console.log(`Authenticated: ${dim("no (no API key)")}`);
|
|
491
|
+
}
|
|
492
|
+
} catch {
|
|
493
|
+
console.log(`API Reachable: ${error("no")}`);
|
|
494
|
+
}
|
|
495
|
+
console.log("");
|
|
496
|
+
console.log(heading("Local Platforms Detected"));
|
|
497
|
+
console.log("\u2500".repeat(40));
|
|
498
|
+
const platforms = [
|
|
499
|
+
{ name: "Claude", check: existsSync3(".claude") || existsSync3("CLAUDE.md") },
|
|
500
|
+
{ name: "Cursor", check: existsSync3(".cursor") },
|
|
501
|
+
{ name: "Node.js", check: existsSync3("node_modules") || existsSync3("package.json") }
|
|
502
|
+
];
|
|
503
|
+
for (const p of platforms) {
|
|
504
|
+
console.log(` ${p.name}: ${p.check ? success("detected") : dim("not found")}`);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/commands/stats.ts
|
|
510
|
+
var PRIMITIVES = ["executable", "knowledge", "connector", "composition", "reference"];
|
|
511
|
+
function registerStatsCommand(program2) {
|
|
512
|
+
program2.command("stats").description("Show CAIK ecosystem statistics").addHelpText("after", `
|
|
513
|
+
Examples:
|
|
514
|
+
caik stats
|
|
515
|
+
caik stats --json`).action(async () => {
|
|
516
|
+
const globalOpts = program2.opts();
|
|
517
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
518
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
519
|
+
const spinner = createSpinner("Fetching stats...");
|
|
520
|
+
if (!globalOpts.json) spinner.start();
|
|
521
|
+
const counts = {};
|
|
522
|
+
let totalInstalls = 0;
|
|
523
|
+
await Promise.all(
|
|
524
|
+
PRIMITIVES.map(async (primitive) => {
|
|
525
|
+
try {
|
|
526
|
+
const result = await client.get(
|
|
527
|
+
`/artifacts/by-primitive/${primitive}`,
|
|
528
|
+
{ limit: "1", offset: "0" }
|
|
529
|
+
);
|
|
530
|
+
counts[primitive] = result.total;
|
|
531
|
+
} catch {
|
|
532
|
+
counts[primitive] = 0;
|
|
533
|
+
}
|
|
534
|
+
})
|
|
535
|
+
);
|
|
536
|
+
const totalArtifacts = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
537
|
+
if (!globalOpts.json) spinner.stop();
|
|
538
|
+
if (globalOpts.json) {
|
|
539
|
+
outputResult({ totalArtifacts, byPrimitive: counts, totalInstalls }, { json: true });
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
console.log(heading("CAIK Registry Stats"));
|
|
543
|
+
console.log("\u2550".repeat(40));
|
|
544
|
+
console.log(`Total artifacts: ${formatNumber(totalArtifacts)}`);
|
|
545
|
+
console.log("");
|
|
546
|
+
console.log("By primitive:");
|
|
547
|
+
for (const [primitive, count] of Object.entries(counts)) {
|
|
548
|
+
const label = primitive.charAt(0).toUpperCase() + primitive.slice(1);
|
|
549
|
+
console.log(` ${label.padEnd(16)} ${formatNumber(count)}`);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/commands/publish.ts
|
|
555
|
+
import { readFileSync as readFileSync2, existsSync as existsSync4 } from "fs";
|
|
556
|
+
function registerPublishCommand(program2) {
|
|
557
|
+
program2.command("publish [path]").description("Publish an artifact to the CAIK registry").requiredOption("--name <name>", "Artifact name").requiredOption("--description <desc>", "Artifact description (min 20 chars)").option("--slug <slug>", "Custom slug (auto-generated from name if omitted)").option("--primitive <type>", "Primitive type", "executable").option("--platform <platform>", "Target platform(s) (comma-separated)", "claude").option("--tag <tag>", "Add a tag (can be repeated)", (val, prev) => [...prev, val], []).option("--source-url <url>", "Source code URL").addHelpText("after", `
|
|
558
|
+
Examples:
|
|
559
|
+
caik publish ./my-skill --name "My Skill" --description "A useful skill for Claude" --tag productivity
|
|
560
|
+
caik publish --name "Auth Helper" --description "Authentication middleware helper" --primitive knowledge`).action(async (path, opts) => {
|
|
561
|
+
const globalOpts = program2.opts();
|
|
562
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
563
|
+
if (!apiKey) {
|
|
564
|
+
throw new CaikError("Authentication required to publish.", "Run 'caik init --auth' to sign in.");
|
|
565
|
+
}
|
|
566
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
567
|
+
let content;
|
|
568
|
+
if (path) {
|
|
569
|
+
if (!existsSync4(path)) {
|
|
570
|
+
throw new CaikError(`File not found: ${path}`);
|
|
571
|
+
}
|
|
572
|
+
content = readFileSync2(path, "utf-8");
|
|
573
|
+
}
|
|
574
|
+
const tags = opts.tag.length > 0 ? opts.tag : ["general"];
|
|
575
|
+
const platforms = opts.platform.split(",").map((p) => p.trim());
|
|
576
|
+
const spinner = createSpinner("Publishing artifact...");
|
|
577
|
+
if (!globalOpts.json) spinner.start();
|
|
578
|
+
const result = await client.post("/artifacts", {
|
|
579
|
+
slug: opts.slug,
|
|
580
|
+
name: opts.name,
|
|
581
|
+
description: opts.description,
|
|
582
|
+
primitive: opts.primitive,
|
|
583
|
+
platforms,
|
|
584
|
+
tags,
|
|
585
|
+
content,
|
|
586
|
+
sourceUrl: opts.sourceUrl
|
|
587
|
+
});
|
|
588
|
+
if (!globalOpts.json) spinner.stop();
|
|
589
|
+
if (globalOpts.json) {
|
|
590
|
+
outputResult(result, { json: true });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
console.log(success(`Published "${result.name}"`));
|
|
594
|
+
console.log(info(`Slug: ${result.slug}`));
|
|
595
|
+
console.log(info(`Status: ${result.status}`));
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/commands/review-queue.ts
|
|
600
|
+
function registerReviewQueueCommand(program2) {
|
|
601
|
+
program2.command("review-queue").description("List artifacts pending review (admin only)").option("--limit <n>", "Maximum results", "20").option("--offset <n>", "Skip first N results", "0").action(async (opts) => {
|
|
602
|
+
const globalOpts = program2.opts();
|
|
603
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
604
|
+
if (!apiKey) {
|
|
605
|
+
throw new CaikError("Authentication required.", "Run 'caik init --auth' to sign in.");
|
|
606
|
+
}
|
|
607
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
608
|
+
const spinner = createSpinner("Fetching review queue...");
|
|
609
|
+
if (!globalOpts.json) spinner.start();
|
|
610
|
+
const result = await client.get("/admin/review-queue", {
|
|
611
|
+
limit: opts.limit,
|
|
612
|
+
offset: opts.offset
|
|
613
|
+
});
|
|
614
|
+
if (!globalOpts.json) spinner.stop();
|
|
615
|
+
if (globalOpts.json) {
|
|
616
|
+
outputResult(result, { json: true });
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (result.results.length === 0) {
|
|
620
|
+
console.log("No artifacts pending review.");
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const table = renderTable(
|
|
624
|
+
["Slug", "Name", "Primitive", "Author", "Submitted"],
|
|
625
|
+
result.results.map((r) => [
|
|
626
|
+
r.slug,
|
|
627
|
+
r.name,
|
|
628
|
+
r.primitive,
|
|
629
|
+
r.authorHandle ?? "unknown",
|
|
630
|
+
new Date(r.createdAt).toLocaleDateString()
|
|
631
|
+
])
|
|
632
|
+
);
|
|
633
|
+
console.log(table);
|
|
634
|
+
console.log(`
|
|
635
|
+
${result.total} pending artifact${result.total !== 1 ? "s" : ""}.`);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/commands/approve.ts
|
|
640
|
+
function registerApproveCommand(program2) {
|
|
641
|
+
program2.command("approve <slug>").description("Approve a pending artifact (admin only)").action(async (slug) => {
|
|
642
|
+
const globalOpts = program2.opts();
|
|
643
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
644
|
+
if (!apiKey) {
|
|
645
|
+
throw new CaikError("Authentication required.", "Run 'caik init --auth' to sign in.");
|
|
646
|
+
}
|
|
647
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
648
|
+
const spinner = createSpinner(`Approving ${slug}...`);
|
|
649
|
+
if (!globalOpts.json) spinner.start();
|
|
650
|
+
const result = await client.post(`/admin/approve/${encodeURIComponent(slug)}`);
|
|
651
|
+
if (!globalOpts.json) spinner.stop();
|
|
652
|
+
if (globalOpts.json) {
|
|
653
|
+
outputResult(result, { json: true });
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
console.log(success(`Artifact "${slug}" approved`));
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// src/commands/flag.ts
|
|
661
|
+
function registerFlagCommand(program2) {
|
|
662
|
+
program2.command("flag <slug>").description("Flag an artifact for review").requiredOption("--reason <reason>", "Reason for flagging (min 10 chars)").option("--severity <level>", "Severity level (low, medium, high)", "medium").addHelpText("after", `
|
|
663
|
+
Examples:
|
|
664
|
+
caik flag my-artifact --reason "Contains outdated API calls that may cause errors"
|
|
665
|
+
caik flag bad-skill --reason "Security concern: requests unnecessary permissions" --severity high`).action(async (slug, opts) => {
|
|
666
|
+
const globalOpts = program2.opts();
|
|
667
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
668
|
+
if (!apiKey) {
|
|
669
|
+
throw new CaikError("Authentication required.", "Run 'caik init --auth' to sign in.");
|
|
670
|
+
}
|
|
671
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
672
|
+
const spinner = createSpinner(`Flagging ${slug}...`);
|
|
673
|
+
if (!globalOpts.json) spinner.start();
|
|
674
|
+
const result = await client.post(`/artifacts/${encodeURIComponent(slug)}/flag`, {
|
|
675
|
+
slug,
|
|
676
|
+
reason: opts.reason,
|
|
677
|
+
severity: opts.severity
|
|
678
|
+
});
|
|
679
|
+
if (!globalOpts.json) spinner.stop();
|
|
680
|
+
if (globalOpts.json) {
|
|
681
|
+
outputResult(result, { json: true });
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
console.log(success(`Flagged "${slug}" for review (severity: ${opts.severity})`));
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/commands/alternatives.ts
|
|
689
|
+
function registerAlternativesCommand(program2) {
|
|
690
|
+
program2.command("alternatives <slug>").description("Show alternatives and similar artifacts").addHelpText("after", `
|
|
691
|
+
Examples:
|
|
692
|
+
caik alternatives auth-middleware`).action(async (slug) => {
|
|
693
|
+
const globalOpts = program2.opts();
|
|
694
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
695
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
696
|
+
const spinner = createSpinner(`Finding alternatives for ${slug}...`);
|
|
697
|
+
if (!globalOpts.json) spinner.start();
|
|
698
|
+
const artifact = await client.get(`/artifacts/${encodeURIComponent(slug)}`);
|
|
699
|
+
let similar = { results: [], total: 0, hasMore: false };
|
|
700
|
+
if (artifact.tags && artifact.tags.length > 0) {
|
|
701
|
+
similar = await client.post("/search", {
|
|
702
|
+
query: artifact.name,
|
|
703
|
+
filters: { primitive: artifact.primitive },
|
|
704
|
+
limit: 10
|
|
705
|
+
});
|
|
706
|
+
similar.results = similar.results.filter((a) => a.slug !== slug);
|
|
707
|
+
}
|
|
708
|
+
if (!globalOpts.json) spinner.stop();
|
|
709
|
+
if (globalOpts.json) {
|
|
710
|
+
outputResult({ artifact: { slug: artifact.slug, name: artifact.name }, alternatives: similar.results }, { json: true });
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (similar.results.length === 0) {
|
|
714
|
+
console.log(info(`No alternatives found for "${artifact.name}".`));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const table = renderTable(
|
|
718
|
+
["Name", "Primitive", "Quality", "Installs"],
|
|
719
|
+
similar.results.map((a) => [
|
|
720
|
+
a.slug,
|
|
721
|
+
a.primitive,
|
|
722
|
+
a.qualityScore != null ? String(a.qualityScore) : "\u2014",
|
|
723
|
+
formatNumber(a.installCount)
|
|
724
|
+
])
|
|
725
|
+
);
|
|
726
|
+
console.log(`Alternatives to "${artifact.name}":
|
|
727
|
+
`);
|
|
728
|
+
console.log(table);
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/commands/update.ts
|
|
733
|
+
function registerUpdateCommand(program2) {
|
|
734
|
+
program2.command("update [slug]").description("Check for and apply artifact updates").option("--yes", "Skip confirmation prompt").action(async (slug) => {
|
|
735
|
+
console.log(info("Update checking is not yet available. Reinstall artifacts with 'caik install <slug>' to get the latest version."));
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/commands/uninstall.ts
|
|
740
|
+
function registerUninstallCommand(program2) {
|
|
741
|
+
program2.command("uninstall <slug>").description("Remove an installed artifact").action(async (slug) => {
|
|
742
|
+
console.log(info("Uninstall is not yet available. Manually remove the artifact files to uninstall."));
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/commands/karma.ts
|
|
747
|
+
function registerKarmaCommand(program2) {
|
|
748
|
+
program2.command("karma").description("Show your karma and contribution breakdown").action(async () => {
|
|
749
|
+
const globalOpts = program2.opts();
|
|
750
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
751
|
+
if (!apiKey) {
|
|
752
|
+
throw new CaikError("Authentication required to view karma.", "Run 'caik init --auth' to sign in.");
|
|
753
|
+
}
|
|
754
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
755
|
+
const spinner = createSpinner("Fetching karma...");
|
|
756
|
+
if (!globalOpts.json) spinner.start();
|
|
757
|
+
const karma = await client.get("/me/karma");
|
|
758
|
+
if (!globalOpts.json) spinner.stop();
|
|
759
|
+
if (globalOpts.json) {
|
|
760
|
+
outputResult(karma, { json: true });
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
console.log(heading("Your CAIK Karma"));
|
|
764
|
+
console.log("\u2550".repeat(40));
|
|
765
|
+
console.log(`Tier: ${karma.karmaTier}`);
|
|
766
|
+
console.log(`Total karma: ${formatNumber(karma.karma)}`);
|
|
767
|
+
console.log(`Level: ${karma.contributionLevel}`);
|
|
768
|
+
console.log(`Handle: ${karma.handle}`);
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/index.ts
|
|
773
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
774
|
+
var __dirname = dirname2(__filename);
|
|
775
|
+
var version = "0.0.1";
|
|
776
|
+
try {
|
|
777
|
+
const pkgPath = join2(__dirname, "..", "package.json");
|
|
778
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
779
|
+
version = pkg.version;
|
|
780
|
+
} catch {
|
|
781
|
+
}
|
|
782
|
+
var program = new Command().name("caik").description("CAIK \u2014 Collective AI Knowledge CLI\nSearch, install, and publish AI artifacts from your terminal.").version(version, "-v, --version").option("--api-url <url>", "API base URL (default: https://caik.dev)").option("--api-key <key>", "API key for authentication").option("--verbose", "Show detailed output including API calls").option("--json", "Output raw JSON (for scripting)");
|
|
783
|
+
registerSearchCommand(program);
|
|
784
|
+
registerInstallCommand(program);
|
|
785
|
+
registerInitCommand(program);
|
|
786
|
+
registerStatusCommand(program);
|
|
787
|
+
registerStatsCommand(program);
|
|
788
|
+
registerPublishCommand(program);
|
|
789
|
+
registerReviewQueueCommand(program);
|
|
790
|
+
registerApproveCommand(program);
|
|
791
|
+
registerFlagCommand(program);
|
|
792
|
+
registerAlternativesCommand(program);
|
|
793
|
+
registerUpdateCommand(program);
|
|
794
|
+
registerUninstallCommand(program);
|
|
795
|
+
registerKarmaCommand(program);
|
|
796
|
+
program.exitOverride();
|
|
797
|
+
async function main() {
|
|
798
|
+
try {
|
|
799
|
+
await program.parseAsync(process.argv);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
if (err instanceof CaikError) {
|
|
802
|
+
console.error(chalk2.red(`\u2717 ${err.message}`));
|
|
803
|
+
if (err.suggestion) {
|
|
804
|
+
console.error(chalk2.dim(` ${err.suggestion}`));
|
|
805
|
+
}
|
|
806
|
+
process.exit(err.exitCode);
|
|
807
|
+
}
|
|
808
|
+
if (err instanceof Error && "exitCode" in err) {
|
|
809
|
+
const exitCode = err.exitCode;
|
|
810
|
+
process.exit(exitCode);
|
|
811
|
+
}
|
|
812
|
+
console.error(chalk2.red(`\u2717 Unexpected error: ${err instanceof Error ? err.message : String(err)}`));
|
|
813
|
+
process.exit(1);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@caik.dev/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CAIK CLI — Search, install, and publish AI artifacts from your terminal",
|
|
6
|
+
"bin": {
|
|
7
|
+
"caik": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsx src/index.ts --",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"chalk": "^5.6.2",
|
|
19
|
+
"cli-table3": "^0.6.5",
|
|
20
|
+
"commander": "^14.0.3",
|
|
21
|
+
"ora": "^9.3.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsup": "^8.5.1",
|
|
25
|
+
"tsx": "^4.21.0"
|
|
26
|
+
}
|
|
27
|
+
}
|