@yamo/cli 1.3.13 → 1.3.14
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 +62 -1
- package/dist/commands/audit.js +67 -0
- package/dist/commands/bridge.js +116 -0
- package/dist/commands/config.js +74 -0
- package/dist/commands/download-bundle.js +87 -0
- package/dist/commands/hash.js +29 -0
- package/dist/commands/init.js +38 -0
- package/dist/commands/submit.js +174 -0
- package/dist/index.js +57 -322
- package/dist/interfaces.js +2 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/config.js +67 -0
- package/dist/utils/constants.js +13 -0
- package/dist/utils/format.js +43 -0
- package/dist/utils/spinner.js +38 -0
- package/dist/utils/storage.js +69 -0
- package/dist/utils/validation.js +39 -0
- package/package.json +22 -6
package/README.md
CHANGED
|
@@ -90,6 +90,44 @@ yamo audit block_001
|
|
|
90
90
|
yamo audit block_001 --key "my-secret"
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### `yamo config <action> [key] [value]`
|
|
94
|
+
Manage local configuration and secrets persistently. Actions: `set`, `get`, `list`, `remove`.
|
|
95
|
+
Values are stored in `~/.yamo/config.json`. Sensitive keys (e.g., `PRIVATE_KEY`) are masked in `list`.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Set a configuration value
|
|
99
|
+
yamo config set PRIVATE_KEY 0x...
|
|
100
|
+
|
|
101
|
+
# List current configuration
|
|
102
|
+
yamo config list
|
|
103
|
+
|
|
104
|
+
# Get a specific value
|
|
105
|
+
yamo config get RPC_URL
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### `yamo download-bundle <cid>`
|
|
109
|
+
Downloads a complete YAMO bundle (file + artifacts) from IPFS.
|
|
110
|
+
|
|
111
|
+
* `-o, --output <path>`: Output directory path.
|
|
112
|
+
* `-k, --key <string>`: Decryption key if the bundle is encrypted.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
yamo download-bundle Qm... -o ./downloads/my-bundle
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `yamo bridge <subcommand>`
|
|
119
|
+
Interact with the YAMO bridge/cluster. Requires `YAMO_BRIDGE_URL` to be set.
|
|
120
|
+
|
|
121
|
+
* `kernels`: List connected kernels and their capabilities.
|
|
122
|
+
* `status`: Show bridge cluster status.
|
|
123
|
+
* `invoke <skill> --payload <json>`: Invoke a skill via the bridge.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
yamo bridge status
|
|
127
|
+
yamo bridge kernels
|
|
128
|
+
yamo bridge invoke my-skill -p '{"input": "test"}'
|
|
129
|
+
```
|
|
130
|
+
|
|
93
131
|
## 🔒 Encryption
|
|
94
132
|
|
|
95
133
|
YAMO v1.0 supports optional client-side encryption for IPFS bundles.
|
|
@@ -104,4 +142,27 @@ yamo submit task.yamo --ipfs --encrypt --key "my-secret"
|
|
|
104
142
|
|
|
105
143
|
# Audit encrypted block
|
|
106
144
|
yamo audit block_001 --key "my-secret"
|
|
107
|
-
```
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## 🧪 Testing
|
|
148
|
+
|
|
149
|
+
Run all tests:
|
|
150
|
+
```bash
|
|
151
|
+
npm test
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Run specific test suite:
|
|
155
|
+
```bash
|
|
156
|
+
npm test -- test/hash.test.js
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Test Coverage:**
|
|
160
|
+
- Unit tests: 22 tests (hash, init, validation utilities, format utilities, constants)
|
|
161
|
+
- Integration tests: 8 tests (CLI interface, error handling)
|
|
162
|
+
- E2E tests: 2 tests (basic workflows)
|
|
163
|
+
- Security tests: 13 tests (path traversal protection)
|
|
164
|
+
- Validation tests: 3 tests (blockId format)
|
|
165
|
+
- Auto-fetch tests: 5 tests (previousBlock resolution)
|
|
166
|
+
- Health checks: 2 tests (test suite validation)
|
|
167
|
+
|
|
168
|
+
**Total: 54+ tests** with 100% pass rate
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.auditCommand = auditCommand;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const config_js_1 = require("../utils/config.js");
|
|
9
|
+
const format_js_1 = require("../utils/format.js");
|
|
10
|
+
const spinner_js_1 = require("../utils/spinner.js");
|
|
11
|
+
const constants_js_1 = require("../utils/constants.js");
|
|
12
|
+
/**
|
|
13
|
+
* Audit a block's integrity on the blockchain.
|
|
14
|
+
* @param blockId - Block ID to audit
|
|
15
|
+
* @param options - Command options
|
|
16
|
+
* @param deps - Injected dependencies
|
|
17
|
+
*/
|
|
18
|
+
async function auditCommand(blockId, options, deps) {
|
|
19
|
+
try {
|
|
20
|
+
const chainSpinner = (0, spinner_js_1.createSpinner)(`Fetching Block ${blockId} from chain...`);
|
|
21
|
+
let block;
|
|
22
|
+
try {
|
|
23
|
+
block = await deps.chainClient.getBlock(blockId);
|
|
24
|
+
if (!block) {
|
|
25
|
+
chainSpinner.fail(`[ERROR] Block ${blockId} not found on-chain.`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
chainSpinner.succeed(`[DONE] Found on-chain record for ${blockId}`);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
chainSpinner.fail(`[FAILED] Failed to fetch block ${blockId}`);
|
|
32
|
+
throw e;
|
|
33
|
+
}
|
|
34
|
+
format_js_1.format.detail('Record Details:');
|
|
35
|
+
console.log(` Agent: ${block.agentAddress}`);
|
|
36
|
+
console.log(` Hash: ${block.contentHash}`);
|
|
37
|
+
console.log(` IPFS: ${block.ipfsCID || 'None'}`);
|
|
38
|
+
if (!block.ipfsCID) {
|
|
39
|
+
format_js_1.format.warn('No IPFS CID. Cannot perform deep content audit.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const ipfsSpinner = (0, spinner_js_1.createSpinner)('Fetching content from IPFS...');
|
|
43
|
+
try {
|
|
44
|
+
const key = options.key || config_js_1.config.encryptionKey;
|
|
45
|
+
const content = await deps.ipfsClient.download(block.ipfsCID, key);
|
|
46
|
+
const hash = crypto_1.default.createHash(constants_js_1.CONSTANTS.HASH_ALGORITHM).update(content).digest('hex');
|
|
47
|
+
const calcHash = constants_js_1.CONSTANTS.HEX_PREFIX + hash;
|
|
48
|
+
ipfsSpinner.succeed('[DONE] Content retrieved and hashed');
|
|
49
|
+
console.log(` Calculated: ${calcHash}`);
|
|
50
|
+
if (calcHash === block.contentHash) {
|
|
51
|
+
format_js_1.format.success('✅ INTEGRITY VERIFIED: Content matches chain hash.');
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
format_js_1.format.error('INTEGRITY FAILED: Hash mismatch!');
|
|
55
|
+
console.log(` Expected: ${block.contentHash}`);
|
|
56
|
+
console.log(` Got: ${calcHash}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
ipfsSpinner.fail('[FAILED] IPFS download or hash failed');
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
(0, format_js_1.handleCommandError)(error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bridgeCommand = bridgeCommand;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const config_js_1 = require("../utils/config.js");
|
|
6
|
+
const format_js_1 = require("../utils/format.js");
|
|
7
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
8
|
+
function getBridgeBaseUrl() {
|
|
9
|
+
const url = config_js_1.config.bridgeUrl;
|
|
10
|
+
if (!url) {
|
|
11
|
+
throw new Error('Bridge URL not configured. Set YAMO_BRIDGE_URL env var or run: yamo config set YAMO_BRIDGE_URL http://localhost:4001');
|
|
12
|
+
}
|
|
13
|
+
// Accept ws:// URLs (bridge WebSocket) and convert to http://
|
|
14
|
+
return url.replace(/^ws:\/\//, 'http://').replace(/^wss:\/\//, 'https://').replace(/\/$/, '');
|
|
15
|
+
}
|
|
16
|
+
let _rpcId = 0;
|
|
17
|
+
async function bridgeRpc(baseUrl, method, params = {}) {
|
|
18
|
+
const id = ++_rpcId;
|
|
19
|
+
const body = JSON.stringify({ jsonrpc: '2.0', method, params, id });
|
|
20
|
+
const resp = await fetch(`${baseUrl}/rpc`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
body,
|
|
24
|
+
});
|
|
25
|
+
if (!resp.ok) {
|
|
26
|
+
throw new Error(`Bridge HTTP ${resp.status}: ${resp.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
const json = (await resp.json());
|
|
29
|
+
if (json.error) {
|
|
30
|
+
throw new Error(`Bridge RPC error ${json.error.code}: ${json.error.message}`);
|
|
31
|
+
}
|
|
32
|
+
return json.result;
|
|
33
|
+
}
|
|
34
|
+
// ── Subcommand handlers ────────────────────────────────────────────────────
|
|
35
|
+
async function kernelsHandler() {
|
|
36
|
+
try {
|
|
37
|
+
const baseUrl = getBridgeBaseUrl();
|
|
38
|
+
const result = (await bridgeRpc(baseUrl, 'kernel.list'));
|
|
39
|
+
if (result.count === 0) {
|
|
40
|
+
format_js_1.format.warn('No connected kernels.');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
format_js_1.format.success(`Connected kernels: ${result.count}`);
|
|
44
|
+
for (const k of result.kernels) {
|
|
45
|
+
format_js_1.format.info(`\n Kernel: ${k.kernel_id}`);
|
|
46
|
+
format_js_1.format.detail(` Status: ${k.status || 'idle'}`);
|
|
47
|
+
format_js_1.format.detail(` Last seen: ${k.last_seen || 'unknown'}`);
|
|
48
|
+
format_js_1.format.detail(` Capabilities: ${k.capabilities?.length ? k.capabilities.join(', ') : 'none'}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
(0, format_js_1.handleCommandError)(error, 'bridge kernels');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function statusHandler() {
|
|
56
|
+
try {
|
|
57
|
+
const baseUrl = getBridgeBaseUrl();
|
|
58
|
+
const result = (await bridgeRpc(baseUrl, 'cluster.status'));
|
|
59
|
+
format_js_1.format.success('Bridge cluster status');
|
|
60
|
+
format_js_1.format.detail(` Node: ${result.node}`);
|
|
61
|
+
format_js_1.format.detail(` Leader: ${result.leader ?? 'unknown'}`);
|
|
62
|
+
format_js_1.format.detail(` Members: ${result.members?.join(', ') || 'none'}`);
|
|
63
|
+
format_js_1.format.detail(` Connected kernels: ${result.connected_kernels}`);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
(0, format_js_1.handleCommandError)(error, 'bridge status');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function invokeHandler(skill, options) {
|
|
70
|
+
try {
|
|
71
|
+
const baseUrl = getBridgeBaseUrl();
|
|
72
|
+
let payload = {};
|
|
73
|
+
if (options.payload) {
|
|
74
|
+
try {
|
|
75
|
+
payload = JSON.parse(options.payload);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Treat as plain string if not valid JSON
|
|
79
|
+
payload = options.payload;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const timeoutMs = options.timeout ? parseInt(options.timeout, 10) : 30_000;
|
|
83
|
+
format_js_1.format.info(`Invoking skill: ${skill}`);
|
|
84
|
+
const result = (await bridgeRpc(baseUrl, 'skill.invoke', {
|
|
85
|
+
skill,
|
|
86
|
+
payload,
|
|
87
|
+
timeout_ms: timeoutMs,
|
|
88
|
+
}));
|
|
89
|
+
format_js_1.format.success(`Skill invocation complete`);
|
|
90
|
+
format_js_1.format.detail(` Skill: ${result.skill}`);
|
|
91
|
+
format_js_1.format.detail(` Handler: ${result.handler_id}`);
|
|
92
|
+
format_js_1.format.value(` Result: ${JSON.stringify(result.result, null, 2)}`);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
(0, format_js_1.handleCommandError)(error, 'bridge invoke');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// ── Command builder ────────────────────────────────────────────────────────
|
|
99
|
+
function bridgeCommand() {
|
|
100
|
+
const bridge = new commander_1.Command('bridge').description('Interact with the YAMO bridge (requires YAMO_BRIDGE_URL)');
|
|
101
|
+
bridge
|
|
102
|
+
.command('kernels')
|
|
103
|
+
.description('List connected kernels and their capabilities')
|
|
104
|
+
.action(kernelsHandler);
|
|
105
|
+
bridge
|
|
106
|
+
.command('status')
|
|
107
|
+
.description('Show bridge cluster status')
|
|
108
|
+
.action(statusHandler);
|
|
109
|
+
bridge
|
|
110
|
+
.command('invoke <skill>')
|
|
111
|
+
.description('Invoke a skill via the bridge')
|
|
112
|
+
.option('-p, --payload <json>', 'JSON payload to pass to the skill handler')
|
|
113
|
+
.option('-t, --timeout <ms>', 'Timeout in milliseconds', '30000')
|
|
114
|
+
.action(invokeHandler);
|
|
115
|
+
return bridge;
|
|
116
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.configCommand = configCommand;
|
|
4
|
+
const storage_js_1 = require("../utils/storage.js");
|
|
5
|
+
const format_js_1 = require("../utils/format.js");
|
|
6
|
+
/**
|
|
7
|
+
* Handles the 'config' command sub-actions.
|
|
8
|
+
*/
|
|
9
|
+
function configCommand(action, key, value) {
|
|
10
|
+
try {
|
|
11
|
+
switch (action) {
|
|
12
|
+
case 'set': {
|
|
13
|
+
if (!key || !value) {
|
|
14
|
+
throw new Error('Usage: yamo config set <key> <value>');
|
|
15
|
+
}
|
|
16
|
+
storage_js_1.storage.set(key, value);
|
|
17
|
+
format_js_1.format.success(`Set ${key} successfully.`);
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
case 'get': {
|
|
21
|
+
if (!key) {
|
|
22
|
+
throw new Error('Usage: yamo config get <key>');
|
|
23
|
+
}
|
|
24
|
+
const val = storage_js_1.storage.get(key);
|
|
25
|
+
if (val) {
|
|
26
|
+
console.log(val);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
format_js_1.format.warn(`Key '${key}' not found.`);
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case 'list': {
|
|
34
|
+
const configData = storage_js_1.storage.read();
|
|
35
|
+
const keys = Object.keys(configData);
|
|
36
|
+
if (keys.length === 0) {
|
|
37
|
+
format_js_1.format.info('No configuration found.');
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
format_js_1.format.detail('Current Configuration:');
|
|
41
|
+
keys.forEach((k) => {
|
|
42
|
+
const lowerKey = k.toLowerCase();
|
|
43
|
+
const displayValue = lowerKey.includes('key') ||
|
|
44
|
+
lowerKey.includes('jwt') ||
|
|
45
|
+
lowerKey.includes('secret') ||
|
|
46
|
+
lowerKey.includes('pass')
|
|
47
|
+
? '********'
|
|
48
|
+
: configData[k];
|
|
49
|
+
console.log(` ${k}: ${displayValue}`);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case 'remove': {
|
|
55
|
+
if (!key) {
|
|
56
|
+
throw new Error('Usage: yamo config remove <key>');
|
|
57
|
+
}
|
|
58
|
+
storage_js_1.storage.remove(key);
|
|
59
|
+
format_js_1.format.success(`Removed ${key} successfully.`);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
default:
|
|
63
|
+
throw new Error('Invalid action. Use set, get, list, or remove.');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof Error) {
|
|
68
|
+
format_js_1.format.error(error.message);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
format_js_1.format.error('Unknown error occurred');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.downloadBundleCommand = downloadBundleCommand;
|
|
40
|
+
const fs_1 = __importDefault(require("fs"));
|
|
41
|
+
const path_1 = __importDefault(require("path"));
|
|
42
|
+
const dotenv = __importStar(require("dotenv"));
|
|
43
|
+
const core_1 = require("@yamo/core");
|
|
44
|
+
const format_js_1 = require("../utils/format.js");
|
|
45
|
+
const constants_js_1 = require("../utils/constants.js");
|
|
46
|
+
dotenv.config();
|
|
47
|
+
/**
|
|
48
|
+
* Download a bundle from IPFS.
|
|
49
|
+
* @param cid - IPFS content identifier
|
|
50
|
+
* @param options - Command options
|
|
51
|
+
*/
|
|
52
|
+
async function downloadBundleCommand(cid, options) {
|
|
53
|
+
try {
|
|
54
|
+
format_js_1.format.info(`Downloading bundle ${cid}...`);
|
|
55
|
+
const ipfs = new core_1.IpfsManager();
|
|
56
|
+
const key = options.key || process.env.YAMO_ENCRYPTION_KEY;
|
|
57
|
+
const bundle = await ipfs.downloadBundle(cid, key);
|
|
58
|
+
// Create output directory
|
|
59
|
+
const outputDir = options.output.replace('<cid>', cid.substring(0, 8));
|
|
60
|
+
if (!fs_1.default.existsSync(outputDir)) {
|
|
61
|
+
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
// Write block.yamo
|
|
64
|
+
fs_1.default.writeFileSync(path_1.default.join(outputDir, constants_js_1.CONSTANTS.DEFAULT_FILENAME), bundle.block);
|
|
65
|
+
format_js_1.format.success(`✓ Downloaded ${constants_js_1.CONSTANTS.DEFAULT_FILENAME}`);
|
|
66
|
+
// Write metadata
|
|
67
|
+
if (bundle.metadata) {
|
|
68
|
+
fs_1.default.writeFileSync(path_1.default.join(outputDir, 'metadata.json'), JSON.stringify(bundle.metadata, null, 2));
|
|
69
|
+
format_js_1.format.success('✓ Downloaded metadata.json');
|
|
70
|
+
}
|
|
71
|
+
// Write artifact files
|
|
72
|
+
for (const [filename, content] of Object.entries(bundle.files)) {
|
|
73
|
+
const filePath = path_1.default.join(outputDir, filename);
|
|
74
|
+
fs_1.default.writeFileSync(filePath, content);
|
|
75
|
+
format_js_1.format.success(`✓ Downloaded ${filename}`);
|
|
76
|
+
}
|
|
77
|
+
format_js_1.format.success(`\nBundle saved to: ${outputDir}`);
|
|
78
|
+
format_js_1.format.detail(`Files: ${1 + Object.keys(bundle.files).length} total`);
|
|
79
|
+
if (bundle.metadata?.hasEncryption) {
|
|
80
|
+
format_js_1.format.warn('🔒 Bundle was decrypted using provided key');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
(0, format_js_1.handleCommandError)(error);
|
|
85
|
+
format_js_1.format.detail('\nIf the bundle is encrypted, provide --key or set YAMO_ENCRYPTION_KEY');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.hashCommand = hashCommand;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
const constants_js_1 = require("../utils/constants.js");
|
|
10
|
+
const format_js_1 = require("../utils/format.js");
|
|
11
|
+
/**
|
|
12
|
+
* Calculate SHA256 hash of a file.
|
|
13
|
+
* @param file - Path to file
|
|
14
|
+
*/
|
|
15
|
+
function hashCommand(file) {
|
|
16
|
+
try {
|
|
17
|
+
if (!fs_1.default.existsSync(file)) {
|
|
18
|
+
throw new Error(`File not found: ${file}`);
|
|
19
|
+
}
|
|
20
|
+
const content = fs_1.default.readFileSync(file, 'utf-8').trim();
|
|
21
|
+
const hash = crypto_1.default.createHash(constants_js_1.CONSTANTS.HASH_ALGORITHM).update(content).digest('hex');
|
|
22
|
+
const bytes32 = constants_js_1.CONSTANTS.HEX_PREFIX + hash;
|
|
23
|
+
format_js_1.format.success('Block Content Hash:');
|
|
24
|
+
format_js_1.format.value(bytes32);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
(0, format_js_1.handleCommandError)(error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.initCommand = initCommand;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const constants_js_1 = require("../utils/constants.js");
|
|
10
|
+
const format_js_1 = require("../utils/format.js");
|
|
11
|
+
/**
|
|
12
|
+
* Initialize a new YAMO block template.
|
|
13
|
+
* @param agentName - Name of the agent
|
|
14
|
+
* @param options - Command options
|
|
15
|
+
*/
|
|
16
|
+
function initCommand(agentName, options) {
|
|
17
|
+
try {
|
|
18
|
+
const template = `
|
|
19
|
+
agent: ${agentName};
|
|
20
|
+
intent: ${options.intent};
|
|
21
|
+
context:
|
|
22
|
+
platform;yamo_v0.5;
|
|
23
|
+
constraints:
|
|
24
|
+
- human_readable;true;
|
|
25
|
+
priority: medium;
|
|
26
|
+
output: result.json;
|
|
27
|
+
meta: hypothesis;Initial hypothesis;
|
|
28
|
+
meta: confidence;0.9;
|
|
29
|
+
log: session_start;timestamp;${new Date().toISOString()};
|
|
30
|
+
handoff: User;
|
|
31
|
+
`.trim();
|
|
32
|
+
fs_1.default.writeFileSync(constants_js_1.CONSTANTS.DEFAULT_FILENAME, template);
|
|
33
|
+
format_js_1.format.success(`Created YAMO template: ${chalk_1.default.bold(constants_js_1.CONSTANTS.DEFAULT_FILENAME)}`);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
(0, format_js_1.handleCommandError)(error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.submitCommand = submitCommand;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
|
+
const config_js_1 = require("../utils/config.js");
|
|
12
|
+
const format_js_1 = require("../utils/format.js");
|
|
13
|
+
const spinner_js_1 = require("../utils/spinner.js");
|
|
14
|
+
const validation_js_1 = require("../utils/validation.js");
|
|
15
|
+
const constants_js_1 = require("../utils/constants.js");
|
|
16
|
+
/**
|
|
17
|
+
* Validate bytes32 hash format
|
|
18
|
+
*/
|
|
19
|
+
function validateBytes32Hash(value, fieldName) {
|
|
20
|
+
if (!(0, validation_js_1.validateBytes32)(value)) {
|
|
21
|
+
throw new Error(`${fieldName} must be a valid bytes32 hash (0x + 64 hex chars). ` +
|
|
22
|
+
`Received: ${value.substring(0, 20)}...` +
|
|
23
|
+
`\nDo NOT include algorithm prefixes like "sha256:"`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate block ID format
|
|
28
|
+
*/
|
|
29
|
+
function validateBlockIdFormat(blockId) {
|
|
30
|
+
if (!blockId)
|
|
31
|
+
throw new Error('blockId is required');
|
|
32
|
+
if (!(0, validation_js_1.validateBlockId)(blockId)) {
|
|
33
|
+
throw new Error(`blockId must follow format {origin}_{workflow} (e.g., 'claude_chain'). Received: ${blockId}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate encryption key strength
|
|
38
|
+
*/
|
|
39
|
+
async function validateEncryptionKey(key) {
|
|
40
|
+
const { validatePasswordStrength } = await import('@yamo/core');
|
|
41
|
+
try {
|
|
42
|
+
validatePasswordStrength(key);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
const errorMessage = e instanceof Error ? e.message : 'Unknown validation error';
|
|
46
|
+
format_js_1.format.error('Password validation failed:');
|
|
47
|
+
format_js_1.format.error(errorMessage);
|
|
48
|
+
format_js_1.format.warn('\nKey requirements:');
|
|
49
|
+
console.error(' • Minimum 12 characters');
|
|
50
|
+
console.error(' • Mix of uppercase, lowercase, numbers, symbols');
|
|
51
|
+
console.error(' • Avoid common patterns (password, 123456, qwerty)');
|
|
52
|
+
throw e;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Prepare files for IPFS upload
|
|
57
|
+
*/
|
|
58
|
+
function prepareIpfsFiles(content, file) {
|
|
59
|
+
const files = [
|
|
60
|
+
{ name: constants_js_1.CONSTANTS.DEFAULT_FILENAME, content },
|
|
61
|
+
];
|
|
62
|
+
const outputMatch = content.match(/output:\s*([^;]+);/);
|
|
63
|
+
if (outputMatch) {
|
|
64
|
+
const artifactName = outputMatch[1].trim();
|
|
65
|
+
const artifactPath = path_1.default.join(path_1.default.dirname(file), artifactName);
|
|
66
|
+
const resolvedPath = path_1.default.resolve(artifactPath);
|
|
67
|
+
const inputDir = path_1.default.resolve(path_1.default.dirname(file));
|
|
68
|
+
// Security: Validate artifact path
|
|
69
|
+
(0, validation_js_1.validateArtifactPath)(artifactName, resolvedPath, inputDir);
|
|
70
|
+
if (fs_1.default.existsSync(resolvedPath)) {
|
|
71
|
+
format_js_1.format.info(`Bundling output: ${artifactName}`);
|
|
72
|
+
files.push({ name: artifactName, content: fs_1.default.readFileSync(resolvedPath, 'utf8') });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return files;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve previous block hash
|
|
79
|
+
*/
|
|
80
|
+
async function resolvePreviousBlock(chainClient, prev) {
|
|
81
|
+
if (prev) {
|
|
82
|
+
validateBytes32Hash(prev, 'previousBlock');
|
|
83
|
+
return prev;
|
|
84
|
+
}
|
|
85
|
+
format_js_1.format.info('[INFO] No previousBlock provided, fetching latest block from chain...');
|
|
86
|
+
const latestHash = await chainClient.getLatestBlockHash();
|
|
87
|
+
if (latestHash && latestHash !== constants_js_1.CONSTANTS.GENESIS_HASH) {
|
|
88
|
+
format_js_1.format.success(`[INFO] Using latest block's contentHash: ${latestHash}`);
|
|
89
|
+
return latestHash;
|
|
90
|
+
}
|
|
91
|
+
format_js_1.format.warn('[INFO] No existing blocks found, using genesis');
|
|
92
|
+
return constants_js_1.CONSTANTS.GENESIS_HASH;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Submit a YAMO block to the blockchain.
|
|
96
|
+
* @param file - Path to YAMO file
|
|
97
|
+
* @param options - Command options
|
|
98
|
+
* @param deps - Injected dependencies (Chain and IPFS clients)
|
|
99
|
+
*/
|
|
100
|
+
async function submitCommand(file, options, deps) {
|
|
101
|
+
try {
|
|
102
|
+
let blockId = options.id;
|
|
103
|
+
// Interactive fallback for blockId
|
|
104
|
+
if (!blockId && process.stdout.isTTY) {
|
|
105
|
+
const answers = await inquirer_1.default.prompt([
|
|
106
|
+
{
|
|
107
|
+
type: 'input',
|
|
108
|
+
name: 'blockId',
|
|
109
|
+
message: 'Enter Unique Block ID (format: {origin}_{workflow}):',
|
|
110
|
+
validate: (input) => ((0, validation_js_1.validateBlockId)(input) ? true : 'Invalid format. Use {origin}_{workflow}'),
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
blockId = answers.blockId;
|
|
114
|
+
}
|
|
115
|
+
if (!blockId) {
|
|
116
|
+
throw new Error('blockId is required. Provide it via --id or interactively.');
|
|
117
|
+
}
|
|
118
|
+
// Validate inputs
|
|
119
|
+
validateBlockIdFormat(blockId);
|
|
120
|
+
// Validate encryption key if needed
|
|
121
|
+
let encryptionKey;
|
|
122
|
+
if (options.encrypt) {
|
|
123
|
+
let key = options.key || config_js_1.config.encryptionKey;
|
|
124
|
+
if (!key && process.stdout.isTTY) {
|
|
125
|
+
const answers = await inquirer_1.default.prompt([
|
|
126
|
+
{
|
|
127
|
+
type: 'password',
|
|
128
|
+
name: 'key',
|
|
129
|
+
message: 'Enter Encryption Key:',
|
|
130
|
+
mask: '*',
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
key = answers.key;
|
|
134
|
+
}
|
|
135
|
+
encryptionKey = config_js_1.validateConfig.requireEncryptionKey(key);
|
|
136
|
+
await validateEncryptionKey(encryptionKey);
|
|
137
|
+
}
|
|
138
|
+
// Calculate content hash
|
|
139
|
+
const content = fs_1.default.readFileSync(file, 'utf8').trim();
|
|
140
|
+
const hash = crypto_1.default.createHash(constants_js_1.CONSTANTS.HASH_ALGORITHM).update(content).digest('hex');
|
|
141
|
+
const contentHash = constants_js_1.CONSTANTS.HEX_PREFIX + hash;
|
|
142
|
+
format_js_1.format.info(`Calculated Hash: ${contentHash}`);
|
|
143
|
+
// Handle IPFS upload
|
|
144
|
+
let ipfsCID;
|
|
145
|
+
if (options.ipfs) {
|
|
146
|
+
const spinner = (0, spinner_js_1.createSpinner)('Preparing and uploading to IPFS...');
|
|
147
|
+
try {
|
|
148
|
+
const files = prepareIpfsFiles(content, file);
|
|
149
|
+
ipfsCID = await deps.ipfsClient.upload({ content, files, encryptionKey });
|
|
150
|
+
spinner.succeed(`[DONE] Uploaded to IPFS`);
|
|
151
|
+
format_js_1.format.info(`IPFS Bundle CID: ${ipfsCID}`);
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
spinner.fail('[FAILED] IPFS upload failed');
|
|
155
|
+
throw e;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Resolve previous block
|
|
159
|
+
const resolvedPreviousBlock = await resolvePreviousBlock(deps.chainClient, options.prev);
|
|
160
|
+
// Submit to blockchain
|
|
161
|
+
const txSpinner = (0, spinner_js_1.createSpinner)(`Submitting Block ${blockId} to blockchain...`);
|
|
162
|
+
try {
|
|
163
|
+
await deps.chainClient.submitBlock(blockId, resolvedPreviousBlock, contentHash, options.consensus, options.ledger, ipfsCID);
|
|
164
|
+
txSpinner.succeed(`[DONE] Block ${blockId} anchored to chain`);
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
txSpinner.fail('[FAILED] Blockchain submission failed');
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
(0, format_js_1.handleCommandError)(error);
|
|
173
|
+
}
|
|
174
|
+
}
|