@prismer/sdk 1.7.3 → 1.7.4
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 +119 -6
- package/dist/cli.js +556 -17
- package/dist/index.d.mts +72 -2
- package/dist/index.d.ts +72 -2
- package/dist/index.js +137 -14
- package/dist/index.mjs +149 -13
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1125,6 +1125,317 @@ var ENVIRONMENTS = {
|
|
|
1125
1125
|
production: "https://prismer.cloud"
|
|
1126
1126
|
};
|
|
1127
1127
|
|
|
1128
|
+
// src/aip.ts
|
|
1129
|
+
var import_aip_sdk = require("@prismer/aip-sdk");
|
|
1130
|
+
var import_aip_sdk2 = require("@prismer/aip-sdk");
|
|
1131
|
+
var import_aip_sdk3 = require("@prismer/aip-sdk");
|
|
1132
|
+
var import_aip_sdk4 = require("@prismer/aip-sdk");
|
|
1133
|
+
|
|
1134
|
+
// src/encryption.ts
|
|
1135
|
+
function getSubtleCrypto() {
|
|
1136
|
+
if (typeof globalThis.crypto?.subtle !== "undefined") {
|
|
1137
|
+
return globalThis.crypto.subtle;
|
|
1138
|
+
}
|
|
1139
|
+
try {
|
|
1140
|
+
const { webcrypto } = require("crypto");
|
|
1141
|
+
return webcrypto.subtle;
|
|
1142
|
+
} catch {
|
|
1143
|
+
throw new Error("No SubtleCrypto available. Requires browser or Node.js 16+.");
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
function getRandomValues(arr) {
|
|
1147
|
+
if (typeof globalThis.crypto?.getRandomValues !== "undefined") {
|
|
1148
|
+
return globalThis.crypto.getRandomValues(arr);
|
|
1149
|
+
}
|
|
1150
|
+
try {
|
|
1151
|
+
const { webcrypto } = require("crypto");
|
|
1152
|
+
return webcrypto.getRandomValues(arr);
|
|
1153
|
+
} catch {
|
|
1154
|
+
throw new Error("No crypto.getRandomValues available.");
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
var subtle = () => getSubtleCrypto();
|
|
1158
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
1159
|
+
var SALT_LENGTH = 16;
|
|
1160
|
+
var IV_LENGTH = 12;
|
|
1161
|
+
var KEY_LENGTH = 256;
|
|
1162
|
+
var _E2EEncryption = class _E2EEncryption {
|
|
1163
|
+
constructor() {
|
|
1164
|
+
this.masterKey = null;
|
|
1165
|
+
this.keyPair = null;
|
|
1166
|
+
this.sessionKeys = /* @__PURE__ */ new Map();
|
|
1167
|
+
// conversationId → AES key
|
|
1168
|
+
this.salt = null;
|
|
1169
|
+
// ─── Pipeline Functions ──────────────────────────────────
|
|
1170
|
+
this.messageCount = 0;
|
|
1171
|
+
this.lastRotation = Date.now();
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Initialize encryption with user passphrase.
|
|
1175
|
+
* Derives a master key via PBKDF2 and generates an ECDH key pair.
|
|
1176
|
+
*
|
|
1177
|
+
* @param passphrase - User passphrase for master key derivation
|
|
1178
|
+
* @param salt - Optional Base64-encoded salt. If omitted, a random 16-byte salt is generated.
|
|
1179
|
+
* Store the salt (via exportSalt()) so you can re-derive the same master key later.
|
|
1180
|
+
*/
|
|
1181
|
+
async init(passphrase, salt) {
|
|
1182
|
+
this.salt = salt ? new Uint8Array(base64ToArrayBuffer(salt)) : getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
1183
|
+
const passphraseKey = await subtle().importKey(
|
|
1184
|
+
"raw",
|
|
1185
|
+
new TextEncoder().encode(passphrase),
|
|
1186
|
+
"PBKDF2",
|
|
1187
|
+
false,
|
|
1188
|
+
["deriveKey"]
|
|
1189
|
+
);
|
|
1190
|
+
this.masterKey = await subtle().deriveKey(
|
|
1191
|
+
{
|
|
1192
|
+
name: "PBKDF2",
|
|
1193
|
+
salt: new Uint8Array(this.salt.buffer, this.salt.byteOffset, this.salt.byteLength).buffer,
|
|
1194
|
+
iterations: PBKDF2_ITERATIONS,
|
|
1195
|
+
hash: "SHA-256"
|
|
1196
|
+
},
|
|
1197
|
+
passphraseKey,
|
|
1198
|
+
{ name: "AES-GCM", length: KEY_LENGTH },
|
|
1199
|
+
false,
|
|
1200
|
+
["encrypt", "decrypt"]
|
|
1201
|
+
);
|
|
1202
|
+
this.keyPair = await subtle().generateKey(
|
|
1203
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
1204
|
+
true,
|
|
1205
|
+
["deriveKey"]
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Export the salt as Base64 string for persistent storage.
|
|
1210
|
+
* You must store this and pass it back to init() to re-derive the same master key.
|
|
1211
|
+
*/
|
|
1212
|
+
exportSalt() {
|
|
1213
|
+
if (!this.salt) throw new Error("E2E not initialized. Call init() first.");
|
|
1214
|
+
return arrayBufferToBase64(this.salt.buffer);
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Export public key for sharing with conversation peers.
|
|
1218
|
+
*/
|
|
1219
|
+
async exportPublicKey() {
|
|
1220
|
+
if (!this.keyPair) throw new Error("E2E not initialized. Call init() first.");
|
|
1221
|
+
return subtle().exportKey("jwk", this.keyPair.publicKey);
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Derive a shared session key for a conversation using ECDH.
|
|
1225
|
+
* Call this with each peer's public key.
|
|
1226
|
+
*/
|
|
1227
|
+
async deriveSessionKey(conversationId, peerPublicKey) {
|
|
1228
|
+
if (!this.keyPair) throw new Error("E2E not initialized. Call init() first.");
|
|
1229
|
+
const importedPeerKey = await subtle().importKey(
|
|
1230
|
+
"jwk",
|
|
1231
|
+
peerPublicKey,
|
|
1232
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
1233
|
+
false,
|
|
1234
|
+
[]
|
|
1235
|
+
);
|
|
1236
|
+
const sessionKey = await subtle().deriveKey(
|
|
1237
|
+
{ name: "ECDH", public: importedPeerKey },
|
|
1238
|
+
this.keyPair.privateKey,
|
|
1239
|
+
{ name: "AES-GCM", length: KEY_LENGTH },
|
|
1240
|
+
false,
|
|
1241
|
+
["encrypt", "decrypt"]
|
|
1242
|
+
);
|
|
1243
|
+
this.sessionKeys.set(conversationId, sessionKey);
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Set a pre-shared session key for a conversation.
|
|
1247
|
+
* Useful when the key is exchanged out-of-band or derived from a group key.
|
|
1248
|
+
*/
|
|
1249
|
+
async setSessionKey(conversationId, rawKey) {
|
|
1250
|
+
const key = await subtle().importKey(
|
|
1251
|
+
"raw",
|
|
1252
|
+
rawKey,
|
|
1253
|
+
{ name: "AES-GCM", length: KEY_LENGTH },
|
|
1254
|
+
false,
|
|
1255
|
+
["encrypt", "decrypt"]
|
|
1256
|
+
);
|
|
1257
|
+
this.sessionKeys.set(conversationId, key);
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Generate a random session key for a conversation.
|
|
1261
|
+
* Returns the raw key bytes for sharing with peers.
|
|
1262
|
+
*/
|
|
1263
|
+
async generateSessionKey(conversationId) {
|
|
1264
|
+
const key = await subtle().generateKey(
|
|
1265
|
+
{ name: "AES-GCM", length: KEY_LENGTH },
|
|
1266
|
+
true,
|
|
1267
|
+
["encrypt", "decrypt"]
|
|
1268
|
+
);
|
|
1269
|
+
this.sessionKeys.set(conversationId, key);
|
|
1270
|
+
return subtle().exportKey("raw", key);
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Encrypt plaintext for a conversation.
|
|
1274
|
+
* Returns base64-encoded ciphertext with prepended IV.
|
|
1275
|
+
*/
|
|
1276
|
+
async encrypt(conversationId, plaintext) {
|
|
1277
|
+
const key = this.sessionKeys.get(conversationId);
|
|
1278
|
+
if (!key) throw new Error(`No session key for conversation ${conversationId}. Call deriveSessionKey() first.`);
|
|
1279
|
+
const iv = getRandomValues(new Uint8Array(IV_LENGTH));
|
|
1280
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
1281
|
+
const ciphertext = await subtle().encrypt(
|
|
1282
|
+
{ name: "AES-GCM", iv },
|
|
1283
|
+
key,
|
|
1284
|
+
encoded
|
|
1285
|
+
);
|
|
1286
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
1287
|
+
combined.set(iv, 0);
|
|
1288
|
+
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
1289
|
+
return arrayBufferToBase64(combined.buffer);
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Decrypt ciphertext from a conversation.
|
|
1293
|
+
* Expects base64-encoded data with prepended IV.
|
|
1294
|
+
*/
|
|
1295
|
+
async decrypt(conversationId, ciphertext) {
|
|
1296
|
+
const key = this.sessionKeys.get(conversationId);
|
|
1297
|
+
if (!key) throw new Error(`No session key for conversation ${conversationId}. Call deriveSessionKey() first.`);
|
|
1298
|
+
const combined = base64ToArrayBuffer(ciphertext);
|
|
1299
|
+
const iv = combined.slice(0, IV_LENGTH);
|
|
1300
|
+
const data = combined.slice(IV_LENGTH);
|
|
1301
|
+
const decrypted = await subtle().decrypt(
|
|
1302
|
+
{ name: "AES-GCM", iv: new Uint8Array(iv) },
|
|
1303
|
+
key,
|
|
1304
|
+
data
|
|
1305
|
+
);
|
|
1306
|
+
return new TextDecoder().decode(decrypted);
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Check if a session key exists for a conversation.
|
|
1310
|
+
*/
|
|
1311
|
+
hasSessionKey(conversationId) {
|
|
1312
|
+
return this.sessionKeys.has(conversationId);
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Remove session key for a conversation.
|
|
1316
|
+
*/
|
|
1317
|
+
removeSessionKey(conversationId) {
|
|
1318
|
+
this.sessionKeys.delete(conversationId);
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Clear all keys and reset state.
|
|
1322
|
+
*/
|
|
1323
|
+
destroy() {
|
|
1324
|
+
this.masterKey = null;
|
|
1325
|
+
this.keyPair = null;
|
|
1326
|
+
this.sessionKeys.clear();
|
|
1327
|
+
this.salt = null;
|
|
1328
|
+
this.messageCount = 0;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* High-level encrypt-for-send pipeline.
|
|
1332
|
+
* Encrypts content, builds metadata, and handles key rotation.
|
|
1333
|
+
*
|
|
1334
|
+
* Returns { encryptedContent, metadata } ready to send.
|
|
1335
|
+
*/
|
|
1336
|
+
async encryptForSend(conversationId, content) {
|
|
1337
|
+
if (!this.hasSessionKey(conversationId)) {
|
|
1338
|
+
throw new Error("No session key for this conversation. Call deriveSessionKey() first.");
|
|
1339
|
+
}
|
|
1340
|
+
const needsRotation = this.shouldRotateKey();
|
|
1341
|
+
const encryptedContent = await this.encrypt(conversationId, content);
|
|
1342
|
+
this.messageCount++;
|
|
1343
|
+
return {
|
|
1344
|
+
encryptedContent,
|
|
1345
|
+
metadata: {
|
|
1346
|
+
encrypted: true,
|
|
1347
|
+
encryptionVersion: 1,
|
|
1348
|
+
...needsRotation && { keyRotationRequested: true }
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* High-level decrypt-on-receive pipeline.
|
|
1354
|
+
* Decrypts content and validates metadata.
|
|
1355
|
+
*/
|
|
1356
|
+
async decryptOnReceive(conversationId, encryptedContent, metadata) {
|
|
1357
|
+
if (!this.hasSessionKey(conversationId)) {
|
|
1358
|
+
throw new Error("No session key for this conversation. Call deriveSessionKey() first.");
|
|
1359
|
+
}
|
|
1360
|
+
return this.decrypt(conversationId, encryptedContent);
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* High-level file encryption pipeline.
|
|
1364
|
+
*/
|
|
1365
|
+
async encryptFile(conversationId, fileData) {
|
|
1366
|
+
const base64Data = arrayBufferToBase64(fileData);
|
|
1367
|
+
const encryptedData = await this.encrypt(conversationId, base64Data);
|
|
1368
|
+
return {
|
|
1369
|
+
encryptedData,
|
|
1370
|
+
metadata: {
|
|
1371
|
+
encrypted: true,
|
|
1372
|
+
encryptionVersion: 1,
|
|
1373
|
+
fileEncrypted: true
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* High-level file decryption pipeline.
|
|
1379
|
+
*/
|
|
1380
|
+
async decryptFile(conversationId, encryptedData) {
|
|
1381
|
+
const base64Data = await this.decrypt(conversationId, encryptedData);
|
|
1382
|
+
return base64ToArrayBuffer(base64Data);
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Check if key rotation is needed (1000 messages or 24 hours).
|
|
1386
|
+
*/
|
|
1387
|
+
shouldRotateKey() {
|
|
1388
|
+
if (this.messageCount >= _E2EEncryption.KEY_ROTATION_THRESHOLD) {
|
|
1389
|
+
return true;
|
|
1390
|
+
}
|
|
1391
|
+
if (Date.now() - this.lastRotation >= _E2EEncryption.KEY_ROTATION_INTERVAL_MS) {
|
|
1392
|
+
return true;
|
|
1393
|
+
}
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Perform key rotation: generate new ECDH keypair and reset counters.
|
|
1398
|
+
* The caller is responsible for re-exchanging keys with peers.
|
|
1399
|
+
*/
|
|
1400
|
+
async rotateKeys() {
|
|
1401
|
+
this.keyPair = await subtle().generateKey(
|
|
1402
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
1403
|
+
false,
|
|
1404
|
+
["deriveKey"]
|
|
1405
|
+
);
|
|
1406
|
+
this.messageCount = 0;
|
|
1407
|
+
this.lastRotation = Date.now();
|
|
1408
|
+
this.sessionKeys.clear();
|
|
1409
|
+
return this.exportPublicKey();
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
_E2EEncryption.KEY_ROTATION_THRESHOLD = 1e3;
|
|
1413
|
+
_E2EEncryption.KEY_ROTATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1414
|
+
var E2EEncryption = _E2EEncryption;
|
|
1415
|
+
function arrayBufferToBase64(buffer) {
|
|
1416
|
+
if (typeof btoa !== "undefined") {
|
|
1417
|
+
const bytes = new Uint8Array(buffer);
|
|
1418
|
+
let binary = "";
|
|
1419
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
1420
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1421
|
+
}
|
|
1422
|
+
return btoa(binary);
|
|
1423
|
+
}
|
|
1424
|
+
return Buffer.from(buffer).toString("base64");
|
|
1425
|
+
}
|
|
1426
|
+
function base64ToArrayBuffer(base64) {
|
|
1427
|
+
if (typeof atob !== "undefined") {
|
|
1428
|
+
const binary = atob(base64);
|
|
1429
|
+
const bytes = new Uint8Array(binary.length);
|
|
1430
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1431
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1432
|
+
}
|
|
1433
|
+
return bytes.buffer;
|
|
1434
|
+
}
|
|
1435
|
+
const buf = Buffer.from(base64, "base64");
|
|
1436
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1128
1439
|
// src/index.ts
|
|
1129
1440
|
var AccountClient = class {
|
|
1130
1441
|
constructor(_r) {
|
|
@@ -1607,11 +1918,11 @@ var EvolutionClient = class {
|
|
|
1607
1918
|
}
|
|
1608
1919
|
/** Delete a gene */
|
|
1609
1920
|
async deleteGene(geneId) {
|
|
1610
|
-
return this._r("DELETE", `/api/im/evolution/genes/${geneId}`);
|
|
1921
|
+
return this._r("DELETE", `/api/im/evolution/genes/${encodeURIComponent(geneId)}`);
|
|
1611
1922
|
}
|
|
1612
1923
|
/** Publish a gene. Pass skipCanary=true to bypass canary validation (MVP/admin). */
|
|
1613
1924
|
async publishGene(geneId, options) {
|
|
1614
|
-
return this._r("POST", `/api/im/evolution/genes/${geneId}/publish`, options?.skipCanary ? { skipCanary: true } : void 0);
|
|
1925
|
+
return this._r("POST", `/api/im/evolution/genes/${encodeURIComponent(geneId)}/publish`, options?.skipCanary ? { skipCanary: true } : void 0);
|
|
1615
1926
|
}
|
|
1616
1927
|
/** Import a published gene */
|
|
1617
1928
|
async importGene(geneId) {
|
|
@@ -1697,6 +2008,14 @@ var EvolutionClient = class {
|
|
|
1697
2008
|
async getSkillContent(slugOrId) {
|
|
1698
2009
|
return this._r("GET", `/api/im/skills/${encodeURIComponent(slugOrId)}/content`);
|
|
1699
2010
|
}
|
|
2011
|
+
/** Create/submit a community skill */
|
|
2012
|
+
async createSkill(input) {
|
|
2013
|
+
return this._r("POST", "/api/im/skills", input);
|
|
2014
|
+
}
|
|
2015
|
+
/** Star a skill (increment community rating) */
|
|
2016
|
+
async starSkill(skillId) {
|
|
2017
|
+
return this._r("POST", `/api/im/skills/${encodeURIComponent(skillId)}/star`);
|
|
2018
|
+
}
|
|
1700
2019
|
/**
|
|
1701
2020
|
* Install a skill and write SKILL.md to local filesystem.
|
|
1702
2021
|
* Combines cloud install + local file sync for Claude Code / OpenClaw / OpenCode.
|
|
@@ -1759,8 +2078,8 @@ var EvolutionClient = class {
|
|
|
1759
2078
|
async uninstallSkillLocal(slugOrId) {
|
|
1760
2079
|
const result = await this.uninstallSkill(slugOrId);
|
|
1761
2080
|
const removedPaths = [];
|
|
1762
|
-
const
|
|
1763
|
-
if (!
|
|
2081
|
+
const slug = safeSlug(slugOrId);
|
|
2082
|
+
if (!slug) return { ...result, data: { uninstalled: result.data?.uninstalled ?? false, removedPaths } };
|
|
1764
2083
|
try {
|
|
1765
2084
|
const fs2 = await import("fs");
|
|
1766
2085
|
const path2 = await import("path");
|
|
@@ -1768,10 +2087,10 @@ var EvolutionClient = class {
|
|
|
1768
2087
|
const home = os2.homedir();
|
|
1769
2088
|
const pluginBase = process.env.PRISMER_PLUGIN_DIR || path2.join(home, ".claude", "plugins", "prismer");
|
|
1770
2089
|
const dirs = [
|
|
1771
|
-
path2.join(home, ".claude", "skills",
|
|
1772
|
-
path2.join(home, ".openclaw", "skills",
|
|
1773
|
-
path2.join(home, ".config", "opencode", "skills",
|
|
1774
|
-
path2.join(pluginBase, "skills",
|
|
2090
|
+
path2.join(home, ".claude", "skills", slug),
|
|
2091
|
+
path2.join(home, ".openclaw", "skills", slug),
|
|
2092
|
+
path2.join(home, ".config", "opencode", "skills", slug),
|
|
2093
|
+
path2.join(pluginBase, "skills", slug)
|
|
1775
2094
|
];
|
|
1776
2095
|
for (const dir of dirs) {
|
|
1777
2096
|
try {
|
|
@@ -1881,6 +2200,9 @@ var EvolutionClient = class {
|
|
|
1881
2200
|
return this._r("POST", "/api/im/evolution/sync", body);
|
|
1882
2201
|
}
|
|
1883
2202
|
};
|
|
2203
|
+
function safeSlug(input) {
|
|
2204
|
+
return input.replace(/[\/\\]/g, "").replace(/\.\./g, "").replace(/\0/g, "");
|
|
2205
|
+
}
|
|
1884
2206
|
function guessMimeType(fileName) {
|
|
1885
2207
|
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
1886
2208
|
const map = {
|
|
@@ -3285,6 +3607,52 @@ function register3(parent, getIMClient2, _getAPIClient) {
|
|
|
3285
3607
|
handleError(err);
|
|
3286
3608
|
}
|
|
3287
3609
|
});
|
|
3610
|
+
evolve.command("publish <gene-id>").description("Publish a private gene to the evolution network").option("--skip-canary", "skip canary phase and publish directly").option("--json", "output raw JSON response").action(async (geneId, opts) => {
|
|
3611
|
+
const client = getIMClient2();
|
|
3612
|
+
try {
|
|
3613
|
+
const res = await client.im.evolution.publishGene(geneId, { skipCanary: opts.skipCanary });
|
|
3614
|
+
if (opts.json) {
|
|
3615
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
3616
|
+
return;
|
|
3617
|
+
}
|
|
3618
|
+
printResult(res, `Gene ${geneId} published${opts.skipCanary ? " (skipped canary)" : " (canary phase)"}`);
|
|
3619
|
+
} catch (err) {
|
|
3620
|
+
handleError(err);
|
|
3621
|
+
}
|
|
3622
|
+
});
|
|
3623
|
+
evolve.command("fork <gene-id>").description("Fork a public gene with optional modifications").option("--strategy <steps...>", "override strategy steps").option("--json", "output raw JSON response").action(async (geneId, opts) => {
|
|
3624
|
+
const client = getIMClient2();
|
|
3625
|
+
try {
|
|
3626
|
+
const res = await client.im.evolution.forkGene({
|
|
3627
|
+
gene_id: geneId,
|
|
3628
|
+
modifications: opts.strategy ? { strategy: opts.strategy } : void 0
|
|
3629
|
+
});
|
|
3630
|
+
if (opts.json) {
|
|
3631
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
3632
|
+
return;
|
|
3633
|
+
}
|
|
3634
|
+
printResult(res);
|
|
3635
|
+
const data = res.data;
|
|
3636
|
+
const newId = data?.id ?? data?.gene_id ?? "unknown";
|
|
3637
|
+
process.stdout.write(`Forked gene ${geneId} \u2192 ${newId}
|
|
3638
|
+
`);
|
|
3639
|
+
} catch (err) {
|
|
3640
|
+
handleError(err);
|
|
3641
|
+
}
|
|
3642
|
+
});
|
|
3643
|
+
evolve.command("delete <gene-id>").description("Delete a gene you own").option("--json", "output raw JSON response").action(async (geneId, opts) => {
|
|
3644
|
+
const client = getIMClient2();
|
|
3645
|
+
try {
|
|
3646
|
+
const res = await client.im.evolution.deleteGene(geneId);
|
|
3647
|
+
if (opts.json) {
|
|
3648
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
printResult(res, `Gene ${geneId} deleted`);
|
|
3652
|
+
} catch (err) {
|
|
3653
|
+
handleError(err);
|
|
3654
|
+
}
|
|
3655
|
+
});
|
|
3288
3656
|
evolve.command("import <gene-id>").description("Import a published gene into your collection").option("--json", "output raw JSON response").action(async (geneId, opts) => {
|
|
3289
3657
|
const client = getIMClient2();
|
|
3290
3658
|
try {
|
|
@@ -4619,7 +4987,7 @@ function readConfig() {
|
|
|
4619
4987
|
}
|
|
4620
4988
|
function writeConfig(config) {
|
|
4621
4989
|
ensureConfigDir();
|
|
4622
|
-
fs.writeFileSync(CONFIG_PATH, TOML.stringify(config), "utf-8");
|
|
4990
|
+
fs.writeFileSync(CONFIG_PATH, TOML.stringify(config), { encoding: "utf-8", mode: 384 });
|
|
4623
4991
|
}
|
|
4624
4992
|
function setNestedValue(obj, dotPath, value) {
|
|
4625
4993
|
const parts = dotPath.split(".");
|
|
@@ -4635,7 +5003,7 @@ function getIMClient() {
|
|
|
4635
5003
|
const cfg = readConfig();
|
|
4636
5004
|
const token = cfg?.auth?.im_token;
|
|
4637
5005
|
if (!token) {
|
|
4638
|
-
console.error('No IM token. Run "prismer register" first.');
|
|
5006
|
+
console.error('No IM token. Run "prismer setup --agent" or "prismer register <username>" first.');
|
|
4639
5007
|
process.exit(1);
|
|
4640
5008
|
}
|
|
4641
5009
|
const env = cfg?.default?.environment || "production";
|
|
@@ -4646,7 +5014,7 @@ function getAPIClient() {
|
|
|
4646
5014
|
const cfg = readConfig();
|
|
4647
5015
|
const apiKey = cfg?.default?.api_key;
|
|
4648
5016
|
if (!apiKey) {
|
|
4649
|
-
console.error('No API key. Run "prismer
|
|
5017
|
+
console.error('No API key. Run "prismer setup" to sign in and get your key.');
|
|
4650
5018
|
process.exit(1);
|
|
4651
5019
|
}
|
|
4652
5020
|
const env = cfg?.default?.environment || "production";
|
|
@@ -4655,20 +5023,191 @@ function getAPIClient() {
|
|
|
4655
5023
|
}
|
|
4656
5024
|
var program = new import_commander.Command();
|
|
4657
5025
|
program.name("prismer").description("Prismer Cloud SDK CLI").version(cliVersion);
|
|
4658
|
-
|
|
4659
|
-
|
|
5026
|
+
async function verifyAndSaveKey(config, apiKey) {
|
|
5027
|
+
if (!apiKey) {
|
|
5028
|
+
console.error("No key provided.");
|
|
5029
|
+
process.exit(1);
|
|
5030
|
+
}
|
|
5031
|
+
if (!apiKey.startsWith("sk-prismer-")) {
|
|
5032
|
+
console.error("Invalid key format. API keys start with sk-prismer-");
|
|
5033
|
+
console.error("Get your key at: https://prismer.cloud/setup");
|
|
5034
|
+
process.exit(1);
|
|
5035
|
+
}
|
|
5036
|
+
const baseUrl = config.default?.base_url || "https://prismer.cloud";
|
|
5037
|
+
try {
|
|
5038
|
+
const res = await fetch(`${baseUrl}/api/version`, {
|
|
5039
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
5040
|
+
});
|
|
5041
|
+
if (res.status === 401) {
|
|
5042
|
+
console.error("API key is invalid or expired.");
|
|
5043
|
+
console.error("Get a new key at: https://prismer.cloud/setup");
|
|
5044
|
+
process.exit(1);
|
|
5045
|
+
}
|
|
5046
|
+
console.log("API key verified \u2713");
|
|
5047
|
+
} catch (err) {
|
|
5048
|
+
console.warn(`Could not verify key (${err.message}). Saving anyway.`);
|
|
5049
|
+
}
|
|
4660
5050
|
if (!config.default) config.default = {};
|
|
4661
5051
|
config.default.api_key = apiKey;
|
|
4662
5052
|
if (!config.default.environment) config.default.environment = "production";
|
|
4663
|
-
if (config.default.base_url === void 0) config.default.base_url = "";
|
|
4664
5053
|
writeConfig(config);
|
|
4665
|
-
console.log("
|
|
5054
|
+
console.log("");
|
|
5055
|
+
console.log("Saved to ~/.prismer/config.toml");
|
|
5056
|
+
console.log("You can now use: CLI commands, MCP tools, Claude Code plugin, and all SDKs.");
|
|
5057
|
+
}
|
|
5058
|
+
function openBrowser(url) {
|
|
5059
|
+
const { execFile } = require("child_process");
|
|
5060
|
+
if (process.platform === "darwin") {
|
|
5061
|
+
execFile("open", [url], (err) => {
|
|
5062
|
+
if (err) console.warn("Could not open browser. Please open the URL above manually.");
|
|
5063
|
+
});
|
|
5064
|
+
} else if (process.platform === "win32") {
|
|
5065
|
+
execFile("cmd.exe", ["/c", "start", "", url], (err) => {
|
|
5066
|
+
if (err) console.warn("Could not open browser. Please open the URL above manually.");
|
|
5067
|
+
});
|
|
5068
|
+
} else {
|
|
5069
|
+
execFile("xdg-open", [url], (err) => {
|
|
5070
|
+
if (err) console.warn("Could not open browser. Please open the URL above manually.");
|
|
5071
|
+
});
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
async function runSetup(opts, apiKey) {
|
|
5075
|
+
const config = readConfig();
|
|
5076
|
+
if (!config.default) config.default = {};
|
|
5077
|
+
const baseUrl = config.default.base_url || "https://prismer.cloud";
|
|
5078
|
+
if (!opts.force && config.default.api_key?.startsWith("sk-prismer-")) {
|
|
5079
|
+
const masked = config.default.api_key.slice(0, 12) + "..." + config.default.api_key.slice(-4);
|
|
5080
|
+
console.log(`Already configured: ${masked}`);
|
|
5081
|
+
console.log("");
|
|
5082
|
+
console.log("To reconfigure, run: prismer setup --force");
|
|
5083
|
+
console.log("To check status: prismer status");
|
|
5084
|
+
return;
|
|
5085
|
+
}
|
|
5086
|
+
if (apiKey) {
|
|
5087
|
+
await verifyAndSaveKey(config, apiKey);
|
|
5088
|
+
return;
|
|
5089
|
+
}
|
|
5090
|
+
if (opts.agent) {
|
|
5091
|
+
if (!opts.force && config.auth?.im_token) {
|
|
5092
|
+
console.log("Already registered as agent (IM token exists).");
|
|
5093
|
+
console.log("For API key access, run: prismer setup");
|
|
5094
|
+
return;
|
|
5095
|
+
}
|
|
5096
|
+
const username = `agent-${Date.now().toString(36)}`;
|
|
5097
|
+
try {
|
|
5098
|
+
const res = await fetch(`${baseUrl}/api/im/register`, {
|
|
5099
|
+
method: "POST",
|
|
5100
|
+
headers: { "Content-Type": "application/json" },
|
|
5101
|
+
body: JSON.stringify({ username, displayName: username, type: "agent" })
|
|
5102
|
+
});
|
|
5103
|
+
const data = await res.json();
|
|
5104
|
+
if (!data.ok) throw new Error(data.error?.message || "Registration failed");
|
|
5105
|
+
if (!config.auth) config.auth = {};
|
|
5106
|
+
config.auth.im_token = data.data?.token;
|
|
5107
|
+
config.auth.im_user_id = data.data?.imUserId || data.data?.userId;
|
|
5108
|
+
config.auth.im_username = data.data?.username || username;
|
|
5109
|
+
writeConfig(config);
|
|
5110
|
+
console.log("Agent registered with free credits \u2713");
|
|
5111
|
+
console.log(` Username: ${config.auth.im_username}`);
|
|
5112
|
+
console.log(` User ID: ${config.auth.im_user_id}`);
|
|
5113
|
+
console.log("");
|
|
5114
|
+
console.log("For full API access, sign in: prismer setup");
|
|
5115
|
+
} catch (err) {
|
|
5116
|
+
console.error(`Agent registration failed: ${err.message}`);
|
|
5117
|
+
console.error("Try signing in instead: prismer setup");
|
|
5118
|
+
process.exit(1);
|
|
5119
|
+
}
|
|
5120
|
+
return;
|
|
5121
|
+
}
|
|
5122
|
+
if (opts.manual) {
|
|
5123
|
+
const setupUrl = `${baseUrl}/setup?utm_source=cli&utm_medium=manual`;
|
|
5124
|
+
console.log("Opening browser to sign in...");
|
|
5125
|
+
console.log(` ${setupUrl}`);
|
|
5126
|
+
console.log("");
|
|
5127
|
+
openBrowser(setupUrl);
|
|
5128
|
+
console.log("After signing in, copy the API key from the page and paste it below.");
|
|
5129
|
+
console.log("");
|
|
5130
|
+
const readline = require("readline");
|
|
5131
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
5132
|
+
rl.question("Paste your API key: ", (key) => {
|
|
5133
|
+
rl.close();
|
|
5134
|
+
verifyAndSaveKey(config, key.trim()).catch((err) => {
|
|
5135
|
+
console.error(`Setup failed: ${err.message}`);
|
|
5136
|
+
process.exit(1);
|
|
5137
|
+
});
|
|
5138
|
+
});
|
|
5139
|
+
return;
|
|
5140
|
+
}
|
|
5141
|
+
const http = require("http");
|
|
5142
|
+
const crypto2 = require("crypto");
|
|
5143
|
+
const state = crypto2.randomBytes(16).toString("hex");
|
|
5144
|
+
let resolved = false;
|
|
5145
|
+
const server = http.createServer((req, res) => {
|
|
5146
|
+
const url = new URL(req.url, `http://localhost`);
|
|
5147
|
+
if (url.pathname === "/callback") {
|
|
5148
|
+
const key = url.searchParams.get("key");
|
|
5149
|
+
const returnedState = url.searchParams.get("state");
|
|
5150
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
5151
|
+
if (!key || !returnedState || returnedState !== state) {
|
|
5152
|
+
res.end('<html><head><meta name="referrer" content="no-referrer"></head><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Setup failed</h2><p>Invalid or missing parameters. Please try again.</p></body></html>');
|
|
5153
|
+
return;
|
|
5154
|
+
}
|
|
5155
|
+
if (!key.startsWith("sk-prismer-")) {
|
|
5156
|
+
res.end('<html><head><meta name="referrer" content="no-referrer"></head><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Invalid key</h2><p>The key format is unexpected. Please try again.</p></body></html>');
|
|
5157
|
+
return;
|
|
5158
|
+
}
|
|
5159
|
+
res.end('<html><head><meta name="referrer" content="no-referrer"></head><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Done!</h2><p>API key received. You can close this tab.</p></body></html>');
|
|
5160
|
+
resolved = true;
|
|
5161
|
+
verifyAndSaveKey(config, key).then(() => {
|
|
5162
|
+
server.close();
|
|
5163
|
+
process.exit(0);
|
|
5164
|
+
}).catch((err) => {
|
|
5165
|
+
console.error(`Setup failed: ${err.message}`);
|
|
5166
|
+
server.close();
|
|
5167
|
+
process.exit(1);
|
|
5168
|
+
});
|
|
5169
|
+
} else {
|
|
5170
|
+
res.writeHead(404);
|
|
5171
|
+
res.end("Not found");
|
|
5172
|
+
}
|
|
5173
|
+
});
|
|
5174
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5175
|
+
const port = server.address().port;
|
|
5176
|
+
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
5177
|
+
const setupUrl = `${baseUrl}/setup?callback=${encodeURIComponent(callbackUrl)}&state=${state}&utm_source=cli&utm_medium=auto`;
|
|
5178
|
+
console.log("Opening browser to sign in...");
|
|
5179
|
+
console.log("");
|
|
5180
|
+
openBrowser(setupUrl);
|
|
5181
|
+
console.log("Waiting for authentication...");
|
|
5182
|
+
console.log("(If the browser didn't open, visit this URL manually:)");
|
|
5183
|
+
console.log(` ${setupUrl}`);
|
|
5184
|
+
console.log("");
|
|
5185
|
+
setTimeout(() => {
|
|
5186
|
+
if (!resolved) {
|
|
5187
|
+
console.error("Timed out waiting for authentication (5 min).");
|
|
5188
|
+
console.error("");
|
|
5189
|
+
console.error("Alternatives:");
|
|
5190
|
+
console.error(" prismer setup --manual Paste key manually");
|
|
5191
|
+
console.error(" prismer setup --agent Register as agent (free credits, no browser)");
|
|
5192
|
+
server.close();
|
|
5193
|
+
process.exit(1);
|
|
5194
|
+
}
|
|
5195
|
+
}, 5 * 60 * 1e3);
|
|
5196
|
+
});
|
|
5197
|
+
}
|
|
5198
|
+
program.command("setup [api-key]").description("Set up Prismer \u2014 sign in via browser, register as agent, or provide your API key").option("--manual", "Paste API key manually instead of browser auto-flow").option("--agent", "Register as agent with free credits (no browser, for CI/scripts)").option("--force", "Reconfigure even if already set up").action(async (apiKey, opts) => {
|
|
5199
|
+
await runSetup(opts, apiKey);
|
|
5200
|
+
});
|
|
5201
|
+
program.command("init [api-key]").description('Alias for "prismer setup" (deprecated, use setup instead)').option("--manual", "Paste API key manually").option("--agent", "Register as agent with free credits").option("--force", "Reconfigure even if already set up").action(async (apiKey, opts) => {
|
|
5202
|
+
console.log('Note: "prismer init" is deprecated. Use "prismer setup" instead.');
|
|
5203
|
+
console.log("");
|
|
5204
|
+
await runSetup(opts, apiKey);
|
|
4666
5205
|
});
|
|
4667
5206
|
program.command("register <username>").description("Register an IM identity and store the token").option("--type <type>", "Identity type: agent or human", "agent").option("--display-name <name>", "Display name").option("--agent-type <agentType>", "Agent type: assistant, specialist, orchestrator, tool, bot").option("--capabilities <caps>", "Comma-separated capabilities").option("--endpoint <url>", "Webhook endpoint URL").option("--webhook-secret <secret>", "Webhook HMAC secret").action(async (username, opts) => {
|
|
4668
5207
|
const config = readConfig();
|
|
4669
5208
|
const apiKey = config.default?.api_key;
|
|
4670
5209
|
if (!apiKey) {
|
|
4671
|
-
console.error('No API key. Run "prismer
|
|
5210
|
+
console.error('No API key. Run "prismer setup" first.');
|
|
4672
5211
|
process.exit(1);
|
|
4673
5212
|
}
|
|
4674
5213
|
const client = new PrismerClient({
|
|
@@ -4766,7 +5305,7 @@ program.command("status").description("Show current config and live info").actio
|
|
|
4766
5305
|
var configCmd = program.command("config").description("Manage config file");
|
|
4767
5306
|
configCmd.command("show").description("Print config file").action(() => {
|
|
4768
5307
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
4769
|
-
console.log('No config file. Run "prismer
|
|
5308
|
+
console.log('No config file. Run "prismer setup" to create one.');
|
|
4770
5309
|
return;
|
|
4771
5310
|
}
|
|
4772
5311
|
console.log(fs.readFileSync(CONFIG_PATH, "utf-8"));
|