@evomap/evolver 1.89.18 → 1.89.20
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/index.js +183 -1
- package/package.json +7 -2
- package/scripts/skill2recipes.js +118 -0
- package/src/adapters/claudeCode.js +8 -39
- package/src/adapters/hookAdapter.js +68 -5
- package/src/adapters/scripts/evolver-session-start.js +98 -0
- package/src/evolve/guards.js +1 -1
- package/src/evolve/pipeline/collect.js +1 -1
- package/src/evolve/pipeline/dispatch.js +1 -1
- package/src/evolve/pipeline/enrich.js +1 -1
- package/src/evolve/pipeline/hub.js +1 -1
- package/src/evolve/pipeline/select.js +1 -1
- package/src/evolve/pipeline/signals.js +1 -1
- package/src/evolve/utils.js +1 -1
- package/src/evolve.js +1 -1
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/antiAbuseTelemetry.js +1 -1
- package/src/gep/autoDistillConv.js +1 -1
- package/src/gep/autoDistillLlm.js +1 -1
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/conversationDistiller.js +1 -1
- package/src/gep/conversationSniffer.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/epigenetics.js +1 -1
- package/src/gep/execBridge.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/hash.js +1 -1
- package/src/gep/hubFetch.js +1 -1
- package/src/gep/hubReview.js +1 -1
- package/src/gep/hubSearch.js +1 -1
- package/src/gep/hubVerify.js +1 -1
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallInject.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/savingsCore.js +1 -1
- package/src/gep/selector.js +1 -1
- package/src/gep/skill2gep.js +329 -43
- package/src/gep/skill2gepAudit.js +303 -0
- package/src/gep/skill2recipes.js +511 -0
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/tokenSavings.js +1 -1
- package/src/gep/trajectoryExport.js +1 -0
- package/src/gep/workspaceKeychain.js +1 -1
- package/src/proxy/extensions/traceControl.js +1 -1
- package/src/proxy/index.js +3 -0
- package/src/proxy/inject.js +1 -1
- package/src/proxy/router/messages_route.js +52 -1
- package/src/proxy/sync/outbound.js +7 -2
- package/src/proxy/trace/extractor.js +1 -1
- package/src/proxy/trace/usage.js +1 -1
package/index.js
CHANGED
|
@@ -2817,6 +2817,165 @@ async function main() {
|
|
|
2817
2817
|
console.log('');
|
|
2818
2818
|
}
|
|
2819
2819
|
|
|
2820
|
+
} else if (command === 'trajectory-export') {
|
|
2821
|
+
const allowPartial = args.includes('--allow-partial');
|
|
2822
|
+
const runtimeSessions = args.includes('--runtime-sessions');
|
|
2823
|
+
// Strict-by-default marking gate (v1): runtime-session discovery only
|
|
2824
|
+
// collects sessions evolver actively marked (session-start hook), minus
|
|
2825
|
+
// those the gateway already captured. These flags open each gate back up.
|
|
2826
|
+
const includeUnmarked = args.includes('--include-unmarked');
|
|
2827
|
+
const includeGatewayCaptured = args.includes('--include-gateway-captured');
|
|
2828
|
+
try {
|
|
2829
|
+
const optionValue = (name) => {
|
|
2830
|
+
let value;
|
|
2831
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
2832
|
+
const arg = args[i];
|
|
2833
|
+
if (arg === name) {
|
|
2834
|
+
if (i + 1 >= args.length || String(args[i + 1]).startsWith('--')) {
|
|
2835
|
+
throw new Error('missing value for ' + name);
|
|
2836
|
+
}
|
|
2837
|
+
value = String(args[i + 1]);
|
|
2838
|
+
} else if (typeof arg === 'string' && arg.startsWith(name + '=')) {
|
|
2839
|
+
value = arg.slice(name.length + 1);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
return value;
|
|
2843
|
+
};
|
|
2844
|
+
const optionValues = (name) => {
|
|
2845
|
+
const values = [];
|
|
2846
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
2847
|
+
const arg = args[i];
|
|
2848
|
+
if (arg === name) {
|
|
2849
|
+
if (i + 1 >= args.length || String(args[i + 1]).startsWith('--')) {
|
|
2850
|
+
throw new Error('missing value for ' + name);
|
|
2851
|
+
}
|
|
2852
|
+
values.push(String(args[i + 1]));
|
|
2853
|
+
} else if (typeof arg === 'string' && arg.startsWith(name + '=')) {
|
|
2854
|
+
values.push(arg.slice(name.length + 1));
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
return values;
|
|
2858
|
+
};
|
|
2859
|
+
const input = optionValue('--input');
|
|
2860
|
+
const output = optionValue('--output');
|
|
2861
|
+
const runtimeSessionDirs = optionValues('--runtime-session-dir');
|
|
2862
|
+
const nodeSecretInline = optionValue('--node-secret');
|
|
2863
|
+
const nodeSecretFile = optionValue('--node-secret-file');
|
|
2864
|
+
const nodeSecretEnv = optionValue('--node-secret-env');
|
|
2865
|
+
const hubPrivateKeyFile = optionValue('--hub-private-key');
|
|
2866
|
+
const nodeSecretKeyringFile = optionValue('--node-secret-keyring');
|
|
2867
|
+
const markedSessionsFile = optionValue('--marked-sessions-file');
|
|
2868
|
+
let nodeSecret;
|
|
2869
|
+
let hubPrivateKey;
|
|
2870
|
+
let nodeSecretKeyring;
|
|
2871
|
+
const nodeSecretSourceCount = [nodeSecretInline, nodeSecretFile, nodeSecretEnv]
|
|
2872
|
+
.filter((value) => value !== undefined).length;
|
|
2873
|
+
if (nodeSecretSourceCount > 1) {
|
|
2874
|
+
throw new Error('use only one node secret source');
|
|
2875
|
+
}
|
|
2876
|
+
if (nodeSecretInline !== undefined) {
|
|
2877
|
+
if (!nodeSecretInline) throw new Error('missing value for --node-secret');
|
|
2878
|
+
if (fs.existsSync(nodeSecretInline)) {
|
|
2879
|
+
try {
|
|
2880
|
+
nodeSecret = fs.readFileSync(nodeSecretInline, 'utf8').trim();
|
|
2881
|
+
} catch (readErr) {
|
|
2882
|
+
const err = new Error('node secret file is not readable: ' + nodeSecretInline);
|
|
2883
|
+
err.cause = readErr;
|
|
2884
|
+
throw err;
|
|
2885
|
+
}
|
|
2886
|
+
if (!nodeSecret) throw new Error('node secret file is empty');
|
|
2887
|
+
} else {
|
|
2888
|
+
nodeSecret = String(nodeSecretInline).trim();
|
|
2889
|
+
if (!nodeSecret) throw new Error('node secret is empty');
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
if (nodeSecretFile !== undefined) {
|
|
2893
|
+
if (!nodeSecretFile) throw new Error('missing path for --node-secret-file');
|
|
2894
|
+
try {
|
|
2895
|
+
nodeSecret = fs.readFileSync(nodeSecretFile, 'utf8').trim();
|
|
2896
|
+
} catch (readErr) {
|
|
2897
|
+
const err = new Error('node secret file is not readable: ' + nodeSecretFile);
|
|
2898
|
+
err.cause = readErr;
|
|
2899
|
+
throw err;
|
|
2900
|
+
}
|
|
2901
|
+
if (!nodeSecret) throw new Error('node secret file is empty');
|
|
2902
|
+
}
|
|
2903
|
+
if (nodeSecretEnv !== undefined) {
|
|
2904
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(nodeSecretEnv)) {
|
|
2905
|
+
throw new Error('invalid environment variable name for --node-secret-env');
|
|
2906
|
+
}
|
|
2907
|
+
nodeSecret = String(process.env[nodeSecretEnv] || '').trim();
|
|
2908
|
+
if (!nodeSecret) throw new Error('node secret environment variable is empty or unset');
|
|
2909
|
+
}
|
|
2910
|
+
if (hubPrivateKeyFile !== undefined) {
|
|
2911
|
+
if (!hubPrivateKeyFile) throw new Error('missing path for --hub-private-key');
|
|
2912
|
+
try {
|
|
2913
|
+
hubPrivateKey = fs.readFileSync(hubPrivateKeyFile, 'utf8');
|
|
2914
|
+
} catch (readErr) {
|
|
2915
|
+
const err = new Error('hub private key file is not readable: ' + hubPrivateKeyFile);
|
|
2916
|
+
err.cause = readErr;
|
|
2917
|
+
throw err;
|
|
2918
|
+
}
|
|
2919
|
+
if (!String(hubPrivateKey || '').trim()) throw new Error('hub private key file is empty');
|
|
2920
|
+
}
|
|
2921
|
+
if (nodeSecretKeyringFile !== undefined) {
|
|
2922
|
+
if (!nodeSecretKeyringFile) throw new Error('missing path for --node-secret-keyring');
|
|
2923
|
+
try {
|
|
2924
|
+
nodeSecretKeyring = JSON.parse(fs.readFileSync(nodeSecretKeyringFile, 'utf8'));
|
|
2925
|
+
} catch (readErr) {
|
|
2926
|
+
const err = new Error('node secret keyring file is not readable or invalid JSON: ' + nodeSecretKeyringFile);
|
|
2927
|
+
err.cause = readErr;
|
|
2928
|
+
throw err;
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
const { writeTrajectories } = require('./src/gep/trajectoryExport');
|
|
2932
|
+
const result = writeTrajectories({
|
|
2933
|
+
input,
|
|
2934
|
+
output,
|
|
2935
|
+
nodeSecret,
|
|
2936
|
+
nodeSecretKeyring,
|
|
2937
|
+
hubPrivateKey,
|
|
2938
|
+
allowPartial,
|
|
2939
|
+
runtimeSessions: runtimeSessions ? true : undefined,
|
|
2940
|
+
runtimeSessionDirs,
|
|
2941
|
+
markedSessionsFile,
|
|
2942
|
+
includeUnmarked: includeUnmarked ? true : undefined,
|
|
2943
|
+
includeGatewayCaptured: includeGatewayCaptured ? true : undefined,
|
|
2944
|
+
});
|
|
2945
|
+
console.log('[trajectory-export] Wrote ' + result.trajectories.length + ' trajector' + (result.trajectories.length === 1 ? 'y' : 'ies') + ' to ' + result.outputPath);
|
|
2946
|
+
console.log('[trajectory-export] Read ' + result.rowsRead + ' trace row(s) and ' + (result.sessionTurnsRead || 0) + ' session turn(s) from ' + (result.filesRead || 0) + ' file(s).');
|
|
2947
|
+
if (result.sessionFilesRead > 0) {
|
|
2948
|
+
console.log('[trajectory-export] Converted ' + result.sessionFilesRead + ' runtime session file(s).');
|
|
2949
|
+
}
|
|
2950
|
+
if (result.runtimeSessionDiscovery && result.runtimeSessionDiscovery.enabled) {
|
|
2951
|
+
console.log('[trajectory-export] Runtime session discovery is local-only; no Hub upload is performed.');
|
|
2952
|
+
console.log('[trajectory-export] Runtime discovery scanned '
|
|
2953
|
+
+ result.runtimeSessionDiscovery.dirsScanned + ' Codex/Claude dir(s), matched '
|
|
2954
|
+
+ result.runtimeSessionDiscovery.filesMatched + ' workspace session file(s).');
|
|
2955
|
+
const mg = result.runtimeSessionDiscovery.markGate;
|
|
2956
|
+
if (mg) {
|
|
2957
|
+
console.log('[trajectory-export] Marking gate: enforce_marked=' + mg.enforceMarked
|
|
2958
|
+
+ ', exclude_gateway_captured=' + mg.excludeGatewayCaptured
|
|
2959
|
+
+ '; marked_registry=' + mg.markedSessionCount
|
|
2960
|
+
+ ', gateway_captured=' + mg.gatewayCapturedCount
|
|
2961
|
+
+ ', excluded_unmarked=' + mg.excludedByMark
|
|
2962
|
+
+ ', excluded_gateway=' + mg.excludedByGateway + '.');
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
if (result.stats) {
|
|
2966
|
+
console.log('[trajectory-export] Scanned ' + result.stats.rowsScanned
|
|
2967
|
+
+ ' row(s); encrypted=' + result.stats.encryptedRows
|
|
2968
|
+
+ ', skipped_missing_secret=' + result.stats.skippedMissingSecret
|
|
2969
|
+
+ ', decrypt_failures=' + result.stats.decryptFailures
|
|
2970
|
+
+ ', invalid_json=' + result.stats.invalidJson
|
|
2971
|
+
+ ', session_invalid_json=' + (result.stats.sessionInvalidJson || 0)
|
|
2972
|
+
+ ', non_prism=' + result.stats.nonPrismSkipped + '.');
|
|
2973
|
+
}
|
|
2974
|
+
} catch (error) {
|
|
2975
|
+
console.error('[trajectory-export] Failed: ' + (error && error.message || error));
|
|
2976
|
+
process.exit(1);
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2820
2979
|
} else if (command === 'webui') {
|
|
2821
2980
|
const portFlag = args.find(a => typeof a === 'string' && a.startsWith('--port='));
|
|
2822
2981
|
const port = portFlag ? Number(portFlag.slice('--port='.length)) : undefined;
|
|
@@ -3296,7 +3455,7 @@ async function main() {
|
|
|
3296
3455
|
}
|
|
3297
3456
|
|
|
3298
3457
|
} else {
|
|
3299
|
-
console.log(`Usage: node index.js [run|/evolve|login|logout|proxy-token|solidify|review|distill|fetch|sync|asset-log|webui|setup-hooks|reuse|publish|recipe|buy|orders|verify|atp|atp-complete|experiment] [--loop]
|
|
3458
|
+
console.log(`Usage: node index.js [run|/evolve|login|logout|proxy-token|solidify|review|distill|fetch|sync|asset-log|trajectory-export|webui|setup-hooks|reuse|publish|recipe|buy|orders|verify|atp|atp-complete|experiment] [--loop]
|
|
3300
3459
|
- login (authorize this device via the hub, gh-auth-login style; stores an OAuth token used instead of node_secret)
|
|
3301
3460
|
- logout (remove the stored OAuth token)
|
|
3302
3461
|
- proxy-token (print the local proxy bearer token for command-backed client auth)
|
|
@@ -3345,6 +3504,29 @@ async function main() {
|
|
|
3345
3504
|
- --last=<N> (show last N entries)
|
|
3346
3505
|
- --since=<ISO_date> (entries after date)
|
|
3347
3506
|
- --json (raw JSON output)
|
|
3507
|
+
- trajectory-export flags:
|
|
3508
|
+
- --input=<path>|--input <path>
|
|
3509
|
+
(proxy trace JSONL, Claude/Codex/Cursor session JSONL, or directory; default: platform trace file)
|
|
3510
|
+
- --output=<path>|--output <path>
|
|
3511
|
+
(output JSONL; default: ./coding-trajectories.jsonl)
|
|
3512
|
+
- --runtime-sessions (local-only: also scan current-workspace Codex/Claude JSONL from ~/.codex/sessions and ~/.claude/projects; env: EVOLVER_TRAJECTORY_RUNTIME_SESSIONS=1)
|
|
3513
|
+
- --runtime-session-dir=<path>|--runtime-session-dir <path>
|
|
3514
|
+
(local-only: extra Codex/Claude runtime session directory; may be repeated)
|
|
3515
|
+
- --include-unmarked (collect runtime sessions even if evolver did not actively mark them; default: strict — only marked sessions are collected; env: EVOLVER_TRAJECTORY_INCLUDE_UNMARKED=1)
|
|
3516
|
+
- --include-gateway-captured (collect runtime sessions even if the proxy gateway already captured them; default: strict — gateway-captured sessions are skipped; env: EVOLVER_TRAJECTORY_INCLUDE_GATEWAY_CAPTURED=1)
|
|
3517
|
+
- --marked-sessions-file=<path>|--marked-sessions-file <path>
|
|
3518
|
+
(override the evolver-marked-sessions registry path; default: <trace-dir>/marked-sessions.jsonl; env: EVOLVER_MARKED_SESSIONS_FILE)
|
|
3519
|
+
- --allow-partial (skip unreadable encrypted rows instead of failing the whole export)
|
|
3520
|
+
- --node-secret=<hex|path>|--node-secret <hex|path>
|
|
3521
|
+
(read node secret from an existing local file, otherwise use the literal value)
|
|
3522
|
+
- --node-secret-file=<path>|--node-secret-file <path>
|
|
3523
|
+
(read node secret from a local file)
|
|
3524
|
+
- --node-secret-env=<name>|--node-secret-env <name>
|
|
3525
|
+
(read node secret from an environment variable)
|
|
3526
|
+
- --node-secret-keyring=<path>|--node-secret-keyring <path>
|
|
3527
|
+
(read versioned node secrets from a JSON file)
|
|
3528
|
+
- --hub-private-key=<path>|--hub-private-key <path>
|
|
3529
|
+
(decrypt hub_key_envelope trace rows with a local private key)
|
|
3348
3530
|
- webui flags:
|
|
3349
3531
|
- --port=<N> (local Web UI port, default 19821)
|
|
3350
3532
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver",
|
|
3
|
-
"version": "1.89.
|
|
3
|
+
"version": "1.89.20",
|
|
4
4
|
"description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@evomap/atp-sdk": "^0.1.0",
|
|
42
42
|
"@evomap/gep-sdk": "^1.5.0",
|
|
43
43
|
"dotenv": "^16.4.7",
|
|
44
|
-
"undici": "^7.
|
|
44
|
+
"undici": "^7.28.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"javascript-obfuscator": "^5.4.1"
|
|
@@ -49,6 +49,11 @@
|
|
|
49
49
|
"optionalDependencies": {
|
|
50
50
|
"@napi-rs/keyring": "^1.1.6"
|
|
51
51
|
},
|
|
52
|
+
"overrides": {
|
|
53
|
+
"@vercel/blob": {
|
|
54
|
+
"undici": "6.27.0"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
52
57
|
"files": [
|
|
53
58
|
"assets/",
|
|
54
59
|
"index.js",
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// skill2recipes -- CLI: hydrolyze + verify a set of Skills into GEP assets,
|
|
5
|
+
// then compose and publish them as a Recipe to the EvoMap market
|
|
6
|
+
// (https://evomap.ai/market?tab=recipes).
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// # From a manifest (recommended -- supports optional/condition per step):
|
|
10
|
+
// node scripts/skill2recipes.js --manifest ./examples/recipe.manifest.json
|
|
11
|
+
//
|
|
12
|
+
// # Inline, ordered by argument position:
|
|
13
|
+
// node scripts/skill2recipes.js --title "Bug Fix Pipeline" \
|
|
14
|
+
// ./skills/triage ./skills/patch ./skills/verify
|
|
15
|
+
//
|
|
16
|
+
// Options:
|
|
17
|
+
// --manifest <path> JSON manifest { title, description, steps:[...] }
|
|
18
|
+
// --title <str> Recipe title (>=3 chars; overrides manifest)
|
|
19
|
+
// --description <str> Recipe description
|
|
20
|
+
// --price <n> price_per_execution (Credits, min 1)
|
|
21
|
+
// --repo-root <path> Where validation commands run (default: cwd)
|
|
22
|
+
// --no-publish Hydrolyze + verify + build asset_ids locally, but do
|
|
23
|
+
// not POST anything to the Hub (dry run).
|
|
24
|
+
// --no-strict Allow steps whose validation falls back to node --version.
|
|
25
|
+
//
|
|
26
|
+
// Env:
|
|
27
|
+
// A2A_NODE_ID (required) registered EvoMap node id -> recipe owner
|
|
28
|
+
// A2A_HUB_URL (default https://evomap.ai)
|
|
29
|
+
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
|
|
33
|
+
process.env.A2A_TRANSPORT = 'http';
|
|
34
|
+
if (!process.env.A2A_HUB_URL) process.env.A2A_HUB_URL = 'https://evomap.ai';
|
|
35
|
+
// Keep stdout pure JSON: silence the "[evolver] Using host git repository"
|
|
36
|
+
// banner that paths.js prints on first require. Human-readable status lines go
|
|
37
|
+
// to stderr (console.error) so `... | jq` on stdout always works.
|
|
38
|
+
if (!process.env.EVOLVER_QUIET_PARENT_GIT) process.env.EVOLVER_QUIET_PARENT_GIT = '1';
|
|
39
|
+
|
|
40
|
+
const skill2recipes = require('../src/gep/skill2recipes');
|
|
41
|
+
|
|
42
|
+
function parseArgs(argv) {
|
|
43
|
+
const out = { _: [], publish: true, strict: true };
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const a = argv[i];
|
|
46
|
+
if (a === '--manifest') out.manifest = argv[++i];
|
|
47
|
+
else if (a === '--title') out.title = argv[++i];
|
|
48
|
+
else if (a === '--description') out.description = argv[++i];
|
|
49
|
+
else if (a === '--price') out.pricePerExecution = Number(argv[++i]);
|
|
50
|
+
else if (a === '--repo-root') out.repoRoot = argv[++i];
|
|
51
|
+
else if (a === '--no-publish') out.publish = false;
|
|
52
|
+
else if (a === '--no-strict') out.strict = false;
|
|
53
|
+
else if (a === '-h' || a === '--help') out.help = true;
|
|
54
|
+
else if (a.startsWith('--')) { console.error('unknown flag: ' + a); process.exit(2); }
|
|
55
|
+
else out._.push(a);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function usage() {
|
|
61
|
+
console.log([
|
|
62
|
+
'skill2recipes -- compose published Skills into a GEP Recipe',
|
|
63
|
+
'',
|
|
64
|
+
'From a manifest:',
|
|
65
|
+
' node scripts/skill2recipes.js --manifest ./examples/recipe.manifest.json',
|
|
66
|
+
'',
|
|
67
|
+
'Inline (ordered by position):',
|
|
68
|
+
' node scripts/skill2recipes.js --title "Bug Fix Pipeline" ./skills/a ./skills/b',
|
|
69
|
+
'',
|
|
70
|
+
'Flags: --title --description --price --repo-root --no-publish --no-strict',
|
|
71
|
+
].join('\n'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function main() {
|
|
75
|
+
const args = parseArgs(process.argv.slice(2));
|
|
76
|
+
if (args.help) { usage(); return; }
|
|
77
|
+
|
|
78
|
+
if (!process.env.A2A_NODE_ID && args.publish) {
|
|
79
|
+
console.error('WARN: A2A_NODE_ID not set -- recipe owner will be derived from a device fingerprint and the Hub may reject it. Register at https://evomap.ai and export A2A_NODE_ID.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let manifest;
|
|
83
|
+
if (args.manifest) {
|
|
84
|
+
const p = path.resolve(args.manifest);
|
|
85
|
+
if (!fs.existsSync(p)) { console.error('manifest not found: ' + p); process.exit(1); }
|
|
86
|
+
try { manifest = JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
87
|
+
catch (e) { console.error('manifest is not valid JSON: ' + e.message); process.exit(1); }
|
|
88
|
+
} else if (args._.length) {
|
|
89
|
+
manifest = { steps: args._ };
|
|
90
|
+
} else {
|
|
91
|
+
usage();
|
|
92
|
+
console.error('\nERROR: provide --manifest <path> or one or more skill paths.');
|
|
93
|
+
process.exit(2);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const res = await skill2recipes.composeRecipeFromSkills(manifest, {
|
|
97
|
+
title: args.title,
|
|
98
|
+
description: args.description,
|
|
99
|
+
pricePerExecution: args.pricePerExecution,
|
|
100
|
+
repoRoot: args.repoRoot,
|
|
101
|
+
publish: args.publish,
|
|
102
|
+
strict: args.strict,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
console.log(JSON.stringify(res, null, 2));
|
|
106
|
+
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
console.error('\nFAILED: ' + (res.reason || 'unknown') + (res.skill_path ? ' at ' + res.skill_path : ''));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
if (res.recipe_id) {
|
|
112
|
+
console.error('\nOK recipe ' + res.recipe_id + (args.publish ? ' published -> ' + res.market_url : ' built (dry run, not published)'));
|
|
113
|
+
} else if (!args.publish) {
|
|
114
|
+
console.error('\nOK dry run complete (no network calls; ' + (res.steps ? res.steps.length : 0) + ' step(s) built, not published)');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
main().catch((e) => { console.error('ERROR:', e && e.message ? e.message : e); console.error(e); process.exit(1); });
|
|
@@ -1,41 +1,20 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts, removeMarkedSection, assertSafeConfigDir, isEvolverHookCommand } = require('./hookAdapter');
|
|
3
|
+
const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts, removeMarkedSection, assertSafeConfigDir, isEvolverHookCommand, buildSafeNodeHookCommand } = require('./hookAdapter');
|
|
4
4
|
|
|
5
5
|
const HOOK_SCRIPTS_DIR_NAME = 'hooks';
|
|
6
6
|
const EVOLVER_MARKER = '<!-- evolver-evolution-memory -->';
|
|
7
7
|
|
|
8
|
-
function shellSingleQuote(value) {
|
|
9
|
-
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function quoteEvalScript(script) {
|
|
13
|
-
if (process.platform === 'win32') {
|
|
14
|
-
if (script.includes('"')) {
|
|
15
|
-
throw new Error('unexpected double quote in generated hook wrapper');
|
|
16
|
-
}
|
|
17
|
-
return `"${script}"`;
|
|
18
|
-
}
|
|
19
|
-
return shellSingleQuote(script);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function buildSafeNodeHookCommand(scriptPath) {
|
|
23
|
-
const encodedPath = Buffer.from(String(scriptPath), 'utf8').toString('base64');
|
|
24
|
-
const js = [
|
|
25
|
-
"const {spawnSync}=require('child_process')",
|
|
26
|
-
`const p=Buffer.from('${encodedPath}','base64').toString('utf8')`,
|
|
27
|
-
"const r=spawnSync(process.execPath,[p],{stdio:'inherit',shell:false})",
|
|
28
|
-
"if(r.error){throw r.error}",
|
|
29
|
-
'process.exit(r.status == null ? 1 : r.status)',
|
|
30
|
-
].join(';');
|
|
31
|
-
return `node -e ${quoteEvalScript(js)} ${path.basename(scriptPath)}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
8
|
function buildClaudeHooks(evolverRoot, configRoot) {
|
|
9
|
+
// Resolve hook scripts to an absolute path rooted at the real config dir so
|
|
10
|
+
// the command works regardless of the cwd Claude Code is launched from
|
|
11
|
+
// (#590). Falls back to the legacy relative base only when configRoot is
|
|
12
|
+
// absent (callers should always pass it).
|
|
35
13
|
const scriptsBase = configRoot
|
|
36
|
-
? path.
|
|
14
|
+
? path.resolve(configRoot, '.claude', 'hooks')
|
|
37
15
|
: path.join('.claude', 'hooks');
|
|
38
|
-
const hookCommand = (scriptName) =>
|
|
16
|
+
const hookCommand = (scriptName) =>
|
|
17
|
+
buildSafeNodeHookCommand(path.join(scriptsBase, scriptName));
|
|
39
18
|
return {
|
|
40
19
|
hooks: {
|
|
41
20
|
SessionStart: [
|
|
@@ -116,16 +95,6 @@ function install({ configRoot, evolverRoot, force }) {
|
|
|
116
95
|
const claudeMdPath = path.join(configRoot, 'CLAUDE.md');
|
|
117
96
|
assertSafeConfigDir(claudeDir, '.claude', { subdirs: [HOOK_SCRIPTS_DIR_NAME] });
|
|
118
97
|
|
|
119
|
-
if (!force && fs.existsSync(settingsPath)) {
|
|
120
|
-
try {
|
|
121
|
-
const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
122
|
-
if (existing._evolver_managed) {
|
|
123
|
-
console.log('[claude-code] Evolver hooks already installed. Use --force to overwrite.');
|
|
124
|
-
return { ok: true, skipped: true };
|
|
125
|
-
}
|
|
126
|
-
} catch { /* proceed */ }
|
|
127
|
-
}
|
|
128
|
-
|
|
129
98
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
130
99
|
|
|
131
100
|
const hooksCfg = buildClaudeHooks(evolverRoot, configRoot);
|
|
@@ -10,6 +10,14 @@ const PLATFORMS = {
|
|
|
10
10
|
opencode: { name: 'opencode', configDir: '.opencode', detector: '.opencode' },
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
// Detect the host agent from runtime environment signals, which are far more
|
|
14
|
+
// reliable than scanning for `.claude` / `.cursor` directories when both exist
|
|
15
|
+
// on disk (e.g. under $HOME). Precedence when signals conflict (#590):
|
|
16
|
+
// 1. strong Claude Code signals (set only by the Claude Code runtime)
|
|
17
|
+
// 2. Cursor signals
|
|
18
|
+
// 3. Codex signals
|
|
19
|
+
// 4. CLAUDE_PROJECT_DIR — a weak Claude alias some hosts (incl. Cursor's
|
|
20
|
+
// Claude integration) export, so it loses to explicit Cursor signals.
|
|
13
21
|
function detectPlatformFromEnv(env = process.env) {
|
|
14
22
|
const hasStrongClaudeSignal = env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT;
|
|
15
23
|
if (hasStrongClaudeSignal) {
|
|
@@ -20,10 +28,19 @@ function detectPlatformFromEnv(env = process.env) {
|
|
|
20
28
|
env.CURSOR_TRACE_ID ||
|
|
21
29
|
env.CURSOR_SESSION_ID ||
|
|
22
30
|
env.CURSOR_PROJECT_DIR ||
|
|
31
|
+
env.CURSOR_AGENT ||
|
|
23
32
|
String(env.TERM_PROGRAM || '').toLowerCase() === 'cursor';
|
|
24
33
|
if (hasCursorSignal) {
|
|
25
34
|
return 'cursor';
|
|
26
35
|
}
|
|
36
|
+
const hasCodexSignal =
|
|
37
|
+
env.CODEX_THREAD_ID ||
|
|
38
|
+
env.CODEX_SHELL ||
|
|
39
|
+
env.CODEX_CI ||
|
|
40
|
+
env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE;
|
|
41
|
+
if (hasCodexSignal) {
|
|
42
|
+
return 'codex';
|
|
43
|
+
}
|
|
27
44
|
if (env.CLAUDE_PROJECT_DIR) {
|
|
28
45
|
return 'claude-code';
|
|
29
46
|
}
|
|
@@ -45,6 +62,37 @@ function detectPlatform(cwd) {
|
|
|
45
62
|
return null;
|
|
46
63
|
}
|
|
47
64
|
|
|
65
|
+
// Quote `value` as a single shell argument for the host that runs hook
|
|
66
|
+
// `command` strings (POSIX sh, or cmd.exe on Windows).
|
|
67
|
+
function shellQuoteForHost(value) {
|
|
68
|
+
const raw = String(value);
|
|
69
|
+
if (process.platform === 'win32') {
|
|
70
|
+
return `"${raw.replace(/"/g, '\\"')}"`;
|
|
71
|
+
}
|
|
72
|
+
return `'${raw.replace(/'/g, "'\\''")}'`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Build a hook `command` that runs `scriptPath` with node WITHOUT exposing the
|
|
76
|
+
// path bytes to the shell. The absolute path is base64-encoded into a `node -e`
|
|
77
|
+
// wrapper that decodes it and spawns the real script with `shell:false`, so
|
|
78
|
+
// spaces / `$()` / backticks / `%VAR%` in the path can never be expanded by sh
|
|
79
|
+
// or cmd.exe (#590). The plaintext basename is appended as a final, inert arg
|
|
80
|
+
// so uninstall/reinstall can still match evolver-owned hooks by command
|
|
81
|
+
// substring (see isEvolverHookCommand) even though the path itself is encoded.
|
|
82
|
+
//
|
|
83
|
+
// Shared across adapters so there is exactly one implementation to audit.
|
|
84
|
+
function buildSafeNodeHookCommand(scriptPath) {
|
|
85
|
+
const encodedPath = Buffer.from(String(scriptPath), 'utf8').toString('base64');
|
|
86
|
+
const js = [
|
|
87
|
+
"const{spawnSync}=require('child_process')",
|
|
88
|
+
`const p=Buffer.from('${encodedPath}','base64').toString('utf8')`,
|
|
89
|
+
"const r=spawnSync(process.execPath,[p],{stdio:'inherit',shell:false})",
|
|
90
|
+
"if(r.error){console.error(r.error.message||String(r.error));process.exit(1)}",
|
|
91
|
+
"process.exit(r.status==null?1:r.status)",
|
|
92
|
+
].join(';');
|
|
93
|
+
return `node -e ${shellQuoteForHost(js)} ${path.basename(scriptPath)}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
48
96
|
function resolveConfigRoot(platformId, cwd) {
|
|
49
97
|
const root = cwd || process.cwd();
|
|
50
98
|
const home = os.homedir();
|
|
@@ -97,11 +145,9 @@ function mergeWithHooksUnion(target, source) {
|
|
|
97
145
|
const tArr = Array.isArray(target.hooks[event]) ? target.hooks[event] : null;
|
|
98
146
|
const sArr = Array.isArray(source.hooks[event]) ? source.hooks[event] : null;
|
|
99
147
|
if (tArr && sArr) {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
};
|
|
104
|
-
const userEntries = tArr.filter(e => !isEvolverOwned(e));
|
|
148
|
+
const userEntries = tArr
|
|
149
|
+
.map(stripEvolverCommands)
|
|
150
|
+
.filter(Boolean);
|
|
105
151
|
result.hooks[event] = [...userEntries, ...sArr];
|
|
106
152
|
}
|
|
107
153
|
}
|
|
@@ -109,6 +155,22 @@ function mergeWithHooksUnion(target, source) {
|
|
|
109
155
|
return result;
|
|
110
156
|
}
|
|
111
157
|
|
|
158
|
+
function stripEvolverCommands(entry) {
|
|
159
|
+
if (!entry || typeof entry !== 'object') return entry;
|
|
160
|
+
if (typeof entry.command === 'string' && isEvolverHookCommand(entry.command)) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (!Array.isArray(entry.hooks)) return entry;
|
|
164
|
+
|
|
165
|
+
const hooks = entry.hooks.filter(h => {
|
|
166
|
+
const cmd = h && h.command;
|
|
167
|
+
return !(typeof cmd === 'string' && isEvolverHookCommand(cmd));
|
|
168
|
+
});
|
|
169
|
+
if (hooks.length === 0) return null;
|
|
170
|
+
if (hooks.length === entry.hooks.length) return entry;
|
|
171
|
+
return { ...entry, hooks };
|
|
172
|
+
}
|
|
173
|
+
|
|
112
174
|
// Pull all `command` strings out of an event entry, supporting both flat
|
|
113
175
|
// shape (Codex: `{type, command}`) and Claude Code matcher shape
|
|
114
176
|
// (`{matcher, hooks: [{type, command}]}`). Returns [] when neither applies.
|
|
@@ -387,6 +449,7 @@ async function setupHooks({ platform, cwd, force, uninstall, evolverRoot } = {})
|
|
|
387
449
|
module.exports = {
|
|
388
450
|
detectPlatformFromEnv,
|
|
389
451
|
detectPlatform,
|
|
452
|
+
buildSafeNodeHookCommand,
|
|
390
453
|
resolveConfigRoot,
|
|
391
454
|
loadAdapter,
|
|
392
455
|
mergeJsonFile,
|
|
@@ -373,6 +373,58 @@ function getNoticeStatePath() {
|
|
|
373
373
|
return path.join(dir, 'session-start-notice-state.json');
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
// Resolve the evolver-marked-sessions registry path. This is the v1 "evolver
|
|
377
|
+
// actively marked this session" ledger: only sessions whose session-start hook
|
|
378
|
+
// actually fired (i.e. sessions evolver participated in, after hook install)
|
|
379
|
+
// land here. The trajectory exporter reads it to gate which runtime-session
|
|
380
|
+
// transcripts get collected (strict by default). Lives alongside the proxy
|
|
381
|
+
// trace dir so both collection ledgers share one home. The env override keeps
|
|
382
|
+
// tests hermetic and lets an operator relocate the ledger.
|
|
383
|
+
function getMarkedSessionsPath() {
|
|
384
|
+
if (process.env.EVOLVER_MARKED_SESSIONS_FILE) return process.env.EVOLVER_MARKED_SESSIONS_FILE;
|
|
385
|
+
const dir = process.env.EVOLVER_SETTINGS_DIR
|
|
386
|
+
|| process.env.EVOLVER_SESSION_STATE_DIR
|
|
387
|
+
|| path.join(os.homedir(), '.evolver');
|
|
388
|
+
return path.join(dir, 'marked-sessions.jsonl');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Pull the tool's session_id out of the hook's stdin payload. Claude Code,
|
|
392
|
+
// Codex, and Cursor all pass `session_id` (Claude Code / Cursor) on the
|
|
393
|
+
// SessionStart stdin JSON; some shapes use `sessionId`. Returns '' when absent
|
|
394
|
+
// so callers can skip the registry write without erroring (Kiro's promptSubmit
|
|
395
|
+
// and hosts that pass no stdin simply don't mark — fail-open by design).
|
|
396
|
+
function _extractHookSessionId(input) {
|
|
397
|
+
if (!input || typeof input !== 'object') return '';
|
|
398
|
+
const raw = input.session_id ?? input.sessionId ?? input.sessionID;
|
|
399
|
+
return typeof raw === 'string' ? raw.trim() : '';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Append one mark record to the registry. Best-effort and idempotent enough for
|
|
403
|
+
// the exporter's needs: the exporter dedupes into a Set, so a session marked on
|
|
404
|
+
// every prompt (Kiro) just appends duplicate lines that collapse on read. We do
|
|
405
|
+
// NOT read-modify-write to dedupe here — that would race across concurrent
|
|
406
|
+
// sessions; append-only is safe and the file is bounded by session count.
|
|
407
|
+
function recordMarkedSession(sessionId, info = {}) {
|
|
408
|
+
const sid = String(sessionId || '').trim();
|
|
409
|
+
if (!sid) return false;
|
|
410
|
+
const file = getMarkedSessionsPath();
|
|
411
|
+
try {
|
|
412
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
413
|
+
const record = {
|
|
414
|
+
session_id: sid,
|
|
415
|
+
...(info.cwd ? { cwd: String(info.cwd) } : {}),
|
|
416
|
+
...(info.source ? { source: String(info.source) } : {}),
|
|
417
|
+
marked_at: new Date().toISOString(),
|
|
418
|
+
};
|
|
419
|
+
fs.appendFileSync(file, JSON.stringify(record) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
420
|
+
return true;
|
|
421
|
+
} catch (_) {
|
|
422
|
+
// Never let a registry write failure break session-start: the context
|
|
423
|
+
// injection (stdout JSON) must always be emitted.
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
376
428
|
// TTL throttle keyed by an arbitrary string. Returns true if `key` fired within
|
|
377
429
|
// the last `ttlMs` (caller should suppress); otherwise records "now" for `key`
|
|
378
430
|
// and returns false. Best-effort: a state read/write failure just means no
|
|
@@ -415,7 +467,50 @@ function shouldSkipInjection() {
|
|
|
415
467
|
return throttled(process.cwd(), ttlMs, getDedupStatePath());
|
|
416
468
|
}
|
|
417
469
|
|
|
470
|
+
// Drain the hook stdin (session context JSON) before running, so we can read the
|
|
471
|
+
// tool's session_id and mark the session in the registry. The host passes the
|
|
472
|
+
// SessionStart payload on stdin; we bound the wait with a short watchdog and
|
|
473
|
+
// fail-open (mark nothing, still emit context) if stdin never closes or isn't a
|
|
474
|
+
// pipe. This mirrors the stdin-draining pattern in evolver-task-recall.js /
|
|
475
|
+
// evolver-session-end.js.
|
|
418
476
|
function main() {
|
|
477
|
+
let done = false;
|
|
478
|
+
let buf = '';
|
|
479
|
+
const finishWithInput = (input) => {
|
|
480
|
+
if (done) return;
|
|
481
|
+
done = true;
|
|
482
|
+
try {
|
|
483
|
+
const sessionId = _extractHookSessionId(input);
|
|
484
|
+
if (sessionId) {
|
|
485
|
+
recordMarkedSession(sessionId, {
|
|
486
|
+
cwd: resolveProjectDir(),
|
|
487
|
+
source: String(process.env.EVOLVER_SESSION_SOURCE || (input && input.source) || '').trim() || undefined,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
} catch (_) { /* marking is best-effort; never block injection */ }
|
|
491
|
+
runInjection();
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// Watchdog: if stdin never ends (host passed no pipe, or hangs), proceed
|
|
495
|
+
// without a session_id rather than stalling the agent's session start.
|
|
496
|
+
const watchdog = setTimeout(() => finishWithInput(null), 1500);
|
|
497
|
+
try {
|
|
498
|
+
process.stdin.setEncoding('utf8');
|
|
499
|
+
} catch (_) { /* some hosts pass no stdin */ }
|
|
500
|
+
process.stdin.on('data', (c) => { buf += c; });
|
|
501
|
+
process.stdin.on('error', () => { clearTimeout(watchdog); finishWithInput(null); });
|
|
502
|
+
process.stdin.on('end', () => {
|
|
503
|
+
clearTimeout(watchdog);
|
|
504
|
+
let input = null;
|
|
505
|
+
try { input = buf.trim() ? JSON.parse(buf) : null; } catch (_) { input = null; }
|
|
506
|
+
finishWithInput(input);
|
|
507
|
+
});
|
|
508
|
+
// Nudge a resume so a paused stdin stream flushes its data/end events; the
|
|
509
|
+
// watchdog covers the case where neither ever arrives (no pipe attached).
|
|
510
|
+
try { process.stdin.resume(); } catch (_) { /* ignore */ }
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function runInjection() {
|
|
419
514
|
if (shouldSkipInjection()) {
|
|
420
515
|
process.stdout.write(JSON.stringify({}));
|
|
421
516
|
return;
|
|
@@ -485,5 +580,8 @@ if (require.main === module) {
|
|
|
485
580
|
_proxyExpected,
|
|
486
581
|
_proxyReachable,
|
|
487
582
|
_proxyHealthyIfExpected,
|
|
583
|
+
_extractHookSessionId,
|
|
584
|
+
getMarkedSessionsPath,
|
|
585
|
+
recordMarkedSession,
|
|
488
586
|
};
|
|
489
587
|
}
|