@ujexdev/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +31 -0
- package/dist/beads.js +66 -0
- package/dist/imports.js +84 -0
- package/dist/index.js +575 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Akshay Sarode
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @ujexdev/cli
|
|
2
|
+
|
|
3
|
+
Command-line client for Ujex Postbox, approvals, Recall/Memory, Gateway,
|
|
4
|
+
tools, and audit exports.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g @ujexdev/cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Configure
|
|
13
|
+
|
|
14
|
+
Create an agent in https://app.ujex.dev/settings, then save the one-time device
|
|
15
|
+
key locally:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
ujex init <agentId> <deviceKey>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Use
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
ujex whoami
|
|
25
|
+
ujex send alice@example.com "hello" "Sent from an agent"
|
|
26
|
+
ujex ask "Approve this action?"
|
|
27
|
+
ujex memory search "invoice preference"
|
|
28
|
+
ujex audit verify ./evidence-bundle
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Run `ujex --help` for the full command list.
|
package/dist/beads.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Beads-shaped JSONL export/import for Recall (plan-v4 Phase 9).
|
|
3
|
+
*
|
|
4
|
+
* Steve Yegge's Beads pattern: agent state as JSONL-in-git, hash IDs,
|
|
5
|
+
* mergeable across agents and sessions. Ujex Recall (Markdown +
|
|
6
|
+
* frontmatter) is already on this side of the trend; this module
|
|
7
|
+
* round-trips Recall ↔ Beads-shaped JSONL so users can keep their
|
|
8
|
+
* Ujex memory in git, diff it in PRs, and merge across branches.
|
|
9
|
+
*
|
|
10
|
+
* JSONL row shape (one per memory file):
|
|
11
|
+
* {
|
|
12
|
+
* id: "blake3:<hash>", // content-addressed id
|
|
13
|
+
* name: "research-notes.md",
|
|
14
|
+
* frontmatter: {type, tags, pinned, description, version},
|
|
15
|
+
* body: "<markdown body>",
|
|
16
|
+
* created_at: "ISO-8601",
|
|
17
|
+
* updated_at: "ISO-8601"
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
22
|
+
function contentHash(name, body) {
|
|
23
|
+
// Substitute sha256 for blake3 — blake3 needs a native binding; sha256
|
|
24
|
+
// ships with node:crypto and is stable enough for content-addressing.
|
|
25
|
+
return 'sha256:' + createHash('sha256').update(name).update('\n').update(body).digest('hex').slice(0, 24);
|
|
26
|
+
}
|
|
27
|
+
export function exportToBeadsJsonl(memories, outPath) {
|
|
28
|
+
const rows = memories.map((m) => {
|
|
29
|
+
// Split frontmatter (---\n...\n---\nbody).
|
|
30
|
+
let fm = {};
|
|
31
|
+
let body = m.content;
|
|
32
|
+
if (m.content.startsWith('---\n')) {
|
|
33
|
+
const end = m.content.indexOf('\n---\n', 4);
|
|
34
|
+
if (end > 0) {
|
|
35
|
+
const fmRaw = m.content.slice(4, end);
|
|
36
|
+
body = m.content.slice(end + 5);
|
|
37
|
+
for (const line of fmRaw.split('\n')) {
|
|
38
|
+
const idx = line.indexOf(':');
|
|
39
|
+
if (idx > 0)
|
|
40
|
+
fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
id: contentHash(m.name, body),
|
|
46
|
+
name: m.name,
|
|
47
|
+
frontmatter: { ...fm, ...(m.metadata ?? {}) },
|
|
48
|
+
body,
|
|
49
|
+
created_at: m.createdAt ?? new Date().toISOString(),
|
|
50
|
+
updated_at: m.updatedAt ?? new Date().toISOString(),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
writeFileSync(outPath, rows.map((r) => JSON.stringify(r)).join('\n') + '\n', 'utf8');
|
|
54
|
+
return { count: rows.length };
|
|
55
|
+
}
|
|
56
|
+
export function importFromBeadsJsonl(inPath) {
|
|
57
|
+
const raw = readFileSync(inPath, 'utf8');
|
|
58
|
+
return raw.split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
59
|
+
}
|
|
60
|
+
export function rowToUjexMemoryWrite(row) {
|
|
61
|
+
const fmLines = Object.entries(row.frontmatter ?? {})
|
|
62
|
+
.map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
|
|
63
|
+
.join('\n');
|
|
64
|
+
const content = `---\n${fmLines}\n---\n${row.body}`;
|
|
65
|
+
return { name: row.name, content };
|
|
66
|
+
}
|
package/dist/imports.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration importers for the `ujex import` subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* ujex import mem0 <export.json> — Mem0 user-memory dump
|
|
6
|
+
* ujex import zep <export.json> — Zep user-graph dump
|
|
7
|
+
* ujex import letta <export.json> — Letta agent state dump
|
|
8
|
+
*
|
|
9
|
+
* Each importer normalizes the source schema, writes Ujex-shaped rows,
|
|
10
|
+
* and audits each migrated row.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
export async function importFile(source, path, call) {
|
|
14
|
+
const raw = readFileSync(path, 'utf8');
|
|
15
|
+
switch (source) {
|
|
16
|
+
case 'mem0': return importMem0(raw, call);
|
|
17
|
+
case 'zep': return importZep(raw, call);
|
|
18
|
+
case 'letta': return importLetta(raw, call);
|
|
19
|
+
default:
|
|
20
|
+
throw new Error(`unknown source: ${source}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function importMem0(raw, call) {
|
|
24
|
+
const data = JSON.parse(raw);
|
|
25
|
+
const memories = data.memories ?? [];
|
|
26
|
+
const warnings = [];
|
|
27
|
+
let written = 0;
|
|
28
|
+
for (const m of memories) {
|
|
29
|
+
const name = `mem0-${m.id}.md`;
|
|
30
|
+
const fm = ['---', 'source: mem0', `mem0_id: ${m.id}`, `created_at: ${m.created_at ?? ''}`, '---', ''].join('\n');
|
|
31
|
+
const content = fm + m.memory;
|
|
32
|
+
try {
|
|
33
|
+
await call('memoryWrite', { name, content, tags: ['mem0-import'] });
|
|
34
|
+
written++;
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
warnings.push(`failed ${m.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { source: 'mem0', itemsRead: memories.length, itemsWritten: written, warnings };
|
|
41
|
+
}
|
|
42
|
+
async function importZep(raw, call) {
|
|
43
|
+
const data = JSON.parse(raw);
|
|
44
|
+
const facts = data.facts ?? [];
|
|
45
|
+
const warnings = [];
|
|
46
|
+
let written = 0;
|
|
47
|
+
for (const f of facts) {
|
|
48
|
+
const name = `zep-${f.uuid}.md`;
|
|
49
|
+
const fm = ['---', 'source: zep', `zep_uuid: ${f.uuid}`, `valid_at: ${f.validAt ?? ''}`, '---', ''].join('\n');
|
|
50
|
+
try {
|
|
51
|
+
await call('memoryWrite', { name, content: fm + f.fact, tags: ['zep-import'] });
|
|
52
|
+
written++;
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
warnings.push(`failed ${f.uuid}: ${e instanceof Error ? e.message : String(e)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { source: 'zep', itemsRead: facts.length, itemsWritten: written, warnings };
|
|
59
|
+
}
|
|
60
|
+
async function importLetta(raw, call) {
|
|
61
|
+
const data = JSON.parse(raw);
|
|
62
|
+
const warnings = [];
|
|
63
|
+
let written = 0;
|
|
64
|
+
const all = [
|
|
65
|
+
...(data.archival ?? []).map((m) => ({ kind: 'archival', ...m })),
|
|
66
|
+
...(data.recall ?? []).map((m) => ({ kind: 'recall', ...m })),
|
|
67
|
+
];
|
|
68
|
+
if (data.core?.persona)
|
|
69
|
+
all.push({ kind: 'core_persona', id: 'persona', text: data.core.persona });
|
|
70
|
+
if (data.core?.human)
|
|
71
|
+
all.push({ kind: 'core_human', id: 'human', text: data.core.human });
|
|
72
|
+
for (const m of all) {
|
|
73
|
+
const name = `letta-${m.kind}-${m.id}.md`;
|
|
74
|
+
const fm = ['---', 'source: letta', `letta_kind: ${m.kind}`, `letta_id: ${m.id}`, '---', ''].join('\n');
|
|
75
|
+
try {
|
|
76
|
+
await call('memoryWrite', { name, content: fm + m.text, tags: ['letta-import', `letta-${m.kind}`] });
|
|
77
|
+
written++;
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
warnings.push(`failed ${m.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { source: 'letta', itemsRead: all.length, itemsWritten: written, warnings };
|
|
84
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createHash, createPublicKey, verify as verifySignature } from 'node:crypto';
|
|
6
|
+
import { UjexClient } from '@ujexdev/client';
|
|
7
|
+
import { verifyRecords } from '@ujexdev/audit-chain';
|
|
8
|
+
import { importFile } from './imports.js';
|
|
9
|
+
import { exportToBeadsJsonl, importFromBeadsJsonl, rowToUjexMemoryWrite } from './beads.js';
|
|
10
|
+
const CFG_DIR = join(homedir(), '.config', 'ujex');
|
|
11
|
+
const CFG_PATH = join(CFG_DIR, 'config.json');
|
|
12
|
+
const FIREBASE = {
|
|
13
|
+
apiKey: 'AIzaSyCWJzLBaXapTm9TAqQMmtNWT4GxcPO5H9M',
|
|
14
|
+
authDomain: 'axy-ujex.firebaseapp.com',
|
|
15
|
+
projectId: 'axy-ujex',
|
|
16
|
+
appId: '1:692303724344:web:879a1fb4f4244e783a4bcd',
|
|
17
|
+
};
|
|
18
|
+
function loadConfig() {
|
|
19
|
+
if (!existsSync(CFG_PATH)) {
|
|
20
|
+
console.error(`no config at ${CFG_PATH}. run: ujex init <agentId> <deviceKey>`);
|
|
21
|
+
process.exit(2);
|
|
22
|
+
}
|
|
23
|
+
return JSON.parse(readFileSync(CFG_PATH, 'utf8'));
|
|
24
|
+
}
|
|
25
|
+
function saveConfig(cfg) {
|
|
26
|
+
mkdirSync(CFG_DIR, { recursive: true, mode: 0o700 });
|
|
27
|
+
writeFileSync(CFG_PATH, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
28
|
+
}
|
|
29
|
+
async function client() {
|
|
30
|
+
const c = loadConfig();
|
|
31
|
+
const ap = new UjexClient({ firebase: FIREBASE, agentId: c.agentId, deviceKey: c.deviceKey });
|
|
32
|
+
await ap.connect();
|
|
33
|
+
return ap;
|
|
34
|
+
}
|
|
35
|
+
const HELP = `ujex <command>
|
|
36
|
+
|
|
37
|
+
init <agentId> <deviceKey> save credentials to ~/.config/ujex/config.json
|
|
38
|
+
whoami print agentId and scopes
|
|
39
|
+
send <to> <subject> <body> [--require-human] [--approval-ttl=N]
|
|
40
|
+
send a Postbox email
|
|
41
|
+
ask <prompt> ask the human for approval
|
|
42
|
+
search <query> search Recall/Memory
|
|
43
|
+
memory list
|
|
44
|
+
memory read <name>
|
|
45
|
+
memory write <name> <content>
|
|
46
|
+
memory append <name> <content>
|
|
47
|
+
memory search <query>
|
|
48
|
+
memory index
|
|
49
|
+
|
|
50
|
+
scheduler create <name> <cron> <webhookUrl> [--payload=<json>] [--max-attempts=N]
|
|
51
|
+
scheduler replay <agentId> <jobName> <runId>
|
|
52
|
+
|
|
53
|
+
tools register <agentId> <name> <kind> <url> [--auth-token=<t>] [--connection-id=<id>]
|
|
54
|
+
tools list
|
|
55
|
+
tools invoke <name> [--args=<json>] [--timeout-ms=N]
|
|
56
|
+
tools cred <name> escape hatch — decrypt auth token
|
|
57
|
+
tools connection-register <agentId> <provider> [--scopes=a,b] [--access-token=<t>] [--refresh-token=<t>]
|
|
58
|
+
tools connections [agentId]
|
|
59
|
+
tools revoke-connection <agentId> <connectionId>
|
|
60
|
+
|
|
61
|
+
gateway claim <fqdn> [--mode=duckdns|custom]
|
|
62
|
+
gateway updateIp <fqdn> <ip> (alias: updateIp)
|
|
63
|
+
gateway issue <fqdn> [--staging] duckdns cert (alias: issue)
|
|
64
|
+
gateway acme-start <fqdn> [--staging] custom-domain ACME step 1
|
|
65
|
+
gateway acme-finish <orderId> custom-domain ACME step 2
|
|
66
|
+
gateway duckdns-token <token> human-only: stash DuckDNS token (KMS)
|
|
67
|
+
gateway get-cert <fqdn> [--id=<id>] return PEMs
|
|
68
|
+
|
|
69
|
+
secret set <name> <value> store KMS-encrypted secret
|
|
70
|
+
secret get <name> retrieve secret
|
|
71
|
+
|
|
72
|
+
audit verify <dir|records.jsonl> verify an AAT bundle offline (no path: check live chain)
|
|
73
|
+
audit export-aat --from=ISO --to=ISO [--out=DIR] [--sign-manifest]
|
|
74
|
+
export an IETF AAT bundle (records, manifest, optional signature)
|
|
75
|
+
audit export-iso42005 --from=ISO --to=ISO
|
|
76
|
+
ISO/IEC 42005 evidence pack
|
|
77
|
+
audit list [--limit=N] [--before=SEQ] list my audit entries
|
|
78
|
+
|
|
79
|
+
identity passport <agentId> print signed agent passport
|
|
80
|
+
|
|
81
|
+
import mem0|zep|letta <file.json>
|
|
82
|
+
migrate from a competitor product
|
|
83
|
+
memory export-beads <out.jsonl> export memory to git-mergeable JSONL (Yegge Beads pattern)
|
|
84
|
+
memory import-beads <in.jsonl> re-import a Beads JSONL bundle
|
|
85
|
+
`;
|
|
86
|
+
function flagValue(args, prefix) {
|
|
87
|
+
const found = args.find((a) => a.startsWith(prefix));
|
|
88
|
+
return found?.slice(prefix.length);
|
|
89
|
+
}
|
|
90
|
+
function hasFlag(args, flag) {
|
|
91
|
+
return args.includes(flag);
|
|
92
|
+
}
|
|
93
|
+
function withoutFlags(args) {
|
|
94
|
+
return args.filter((a) => !a.startsWith('--'));
|
|
95
|
+
}
|
|
96
|
+
function parseJSONFlag(args, prefix) {
|
|
97
|
+
const raw = flagValue(args, prefix);
|
|
98
|
+
if (raw === undefined)
|
|
99
|
+
return undefined;
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(raw);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return raw;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function parseJsonl(text) {
|
|
108
|
+
return text
|
|
109
|
+
.split('\n')
|
|
110
|
+
.map((l) => l.trim())
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.map((l) => JSON.parse(l));
|
|
113
|
+
}
|
|
114
|
+
function bundleSha256(text) {
|
|
115
|
+
const body = text.endsWith('\n') ? text.slice(0, -1) : text;
|
|
116
|
+
return createHash('sha256').update(body, 'utf8').digest('hex');
|
|
117
|
+
}
|
|
118
|
+
function canonicalJson(value) {
|
|
119
|
+
if (value === null || typeof value !== 'object')
|
|
120
|
+
return JSON.stringify(value);
|
|
121
|
+
if (Array.isArray(value))
|
|
122
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
123
|
+
const obj = value;
|
|
124
|
+
return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(',')}}`;
|
|
125
|
+
}
|
|
126
|
+
function verifyManifestSignature(manifest, sig) {
|
|
127
|
+
const signature = typeof sig.signature === 'string' ? sig.signature : '';
|
|
128
|
+
const publicKeyPem = typeof sig.publicKeyPem === 'string' ? sig.publicKeyPem : '';
|
|
129
|
+
if (manifest.public_key_id && sig.publicKeyId !== manifest.public_key_id)
|
|
130
|
+
return 'manifest signature key id mismatch';
|
|
131
|
+
if (!signature || !publicKeyPem)
|
|
132
|
+
return 'manifest.sig missing signature/publicKeyPem';
|
|
133
|
+
const ok = verifySignature(null, Buffer.from(canonicalJson(manifest)), createPublicKey(publicKeyPem), Buffer.from(signature, 'base64'));
|
|
134
|
+
return ok ? null : 'manifest signature mismatch';
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Offline AAT-bundle verification — no config, no network, no firebase.
|
|
138
|
+
* Accepts a bundle directory (records.jsonl + optional manifest.json) or a raw
|
|
139
|
+
* records.jsonl. Returns the process exit code. (.zip is handled by the
|
|
140
|
+
* stdlib-only Python verifier, which this points to.)
|
|
141
|
+
*/
|
|
142
|
+
async function offlineAuditVerify(path) {
|
|
143
|
+
if (path.endsWith('.zip')) {
|
|
144
|
+
console.error('The JS CLI does not read .zip bundles. Extract it, or use the stdlib Python verifier:');
|
|
145
|
+
console.error(` pip install ujex-audit-chain && python -m ujex_audit_chain verify ${path}`);
|
|
146
|
+
return 2;
|
|
147
|
+
}
|
|
148
|
+
let recordsText;
|
|
149
|
+
let manifest = null;
|
|
150
|
+
let manifestSig = null;
|
|
151
|
+
try {
|
|
152
|
+
if (existsSync(path) && statSync(path).isDirectory()) {
|
|
153
|
+
recordsText = readFileSync(join(path, 'records.jsonl'), 'utf8');
|
|
154
|
+
const mp = join(path, 'manifest.json');
|
|
155
|
+
if (existsSync(mp))
|
|
156
|
+
manifest = JSON.parse(readFileSync(mp, 'utf8'));
|
|
157
|
+
const sp = join(path, 'manifest.sig');
|
|
158
|
+
if (existsSync(sp))
|
|
159
|
+
manifestSig = JSON.parse(readFileSync(sp, 'utf8'));
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
recordsText = readFileSync(path, 'utf8');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
console.error(`cannot read ${path}: ${e.message}`);
|
|
167
|
+
return 2;
|
|
168
|
+
}
|
|
169
|
+
const rep = await verifyRecords(parseJsonl(recordsText));
|
|
170
|
+
const errors = [...rep.errors];
|
|
171
|
+
let ok = rep.ok;
|
|
172
|
+
if (manifest) {
|
|
173
|
+
if (manifest.head_hash && manifest.head_hash !== rep.headHash) {
|
|
174
|
+
ok = false;
|
|
175
|
+
errors.push('manifest head_hash mismatch');
|
|
176
|
+
}
|
|
177
|
+
if (manifest.bundle_sha256 && manifest.bundle_sha256 !== bundleSha256(recordsText)) {
|
|
178
|
+
ok = false;
|
|
179
|
+
errors.push('manifest bundle_sha256 mismatch');
|
|
180
|
+
}
|
|
181
|
+
if (typeof manifest.record_count === 'number' && manifest.record_count !== rep.recordCount) {
|
|
182
|
+
ok = false;
|
|
183
|
+
errors.push('manifest record_count mismatch');
|
|
184
|
+
}
|
|
185
|
+
if (manifestSig) {
|
|
186
|
+
const sigError = verifyManifestSignature(manifest, manifestSig);
|
|
187
|
+
if (sigError) {
|
|
188
|
+
ok = false;
|
|
189
|
+
errors.push(sigError);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
console.log(`verify: ${ok ? 'OK' : 'FAILED'}`);
|
|
194
|
+
console.log(` format : ${rep.fmt || 'unknown'}`);
|
|
195
|
+
console.log(` records : ${rep.recordCount}`);
|
|
196
|
+
console.log(` head_hash : ${rep.headHash ? rep.headHash.slice(0, 16) + '…' : '(none)'}`);
|
|
197
|
+
if (rep.fmt === 'aat-export') {
|
|
198
|
+
console.log(` links checked : ${rep.linksVerified}`);
|
|
199
|
+
console.log(` seq gaps : ${rep.gaps} (expected for owner-scoped exports)`);
|
|
200
|
+
}
|
|
201
|
+
if (!manifest)
|
|
202
|
+
console.log(' manifest : (none — head_hash is unanchored)');
|
|
203
|
+
else
|
|
204
|
+
console.log(` manifest sig : ${manifestSig ? 'checked' : 'none'}`);
|
|
205
|
+
for (const e of errors)
|
|
206
|
+
console.error(` ERROR : ${e}`);
|
|
207
|
+
return ok ? 0 : 1;
|
|
208
|
+
}
|
|
209
|
+
async function main() {
|
|
210
|
+
const [, , cmd, ...args] = process.argv;
|
|
211
|
+
if (!cmd || cmd === '-h' || cmd === '--help') {
|
|
212
|
+
console.log(HELP);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (cmd === 'init') {
|
|
216
|
+
const [agentId, deviceKey] = args;
|
|
217
|
+
if (!agentId || !deviceKey) {
|
|
218
|
+
console.error('usage: ujex init <agentId> <deviceKey>');
|
|
219
|
+
process.exit(2);
|
|
220
|
+
}
|
|
221
|
+
saveConfig({ agentId, deviceKey });
|
|
222
|
+
console.log(`saved ${CFG_PATH}`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// `audit verify <path>` runs fully offline — no config, no network.
|
|
226
|
+
if (cmd === 'audit' && withoutFlags(args)[0] === 'verify' && withoutFlags(args).length > 1) {
|
|
227
|
+
process.exit(await offlineAuditVerify(withoutFlags(args)[1]));
|
|
228
|
+
}
|
|
229
|
+
const ap = await client();
|
|
230
|
+
switch (cmd) {
|
|
231
|
+
case 'whoami': {
|
|
232
|
+
const c = loadConfig();
|
|
233
|
+
console.log(JSON.stringify({ agentId: c.agentId }, null, 2));
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
case 'send': {
|
|
237
|
+
const [to, subject, ...body] = args;
|
|
238
|
+
if (!to || !subject)
|
|
239
|
+
throw new Error('usage: ujex send <to> <subject> <body>');
|
|
240
|
+
const ttlRaw = flagValue(args, '--approval-ttl=');
|
|
241
|
+
const approvalTtlSec = ttlRaw ? Number(ttlRaw) : undefined;
|
|
242
|
+
const r = await ap.postbox.send({
|
|
243
|
+
to: [to],
|
|
244
|
+
subject,
|
|
245
|
+
body: withoutFlags(body).join(' '),
|
|
246
|
+
requireHuman: hasFlag(args, '--require-human'),
|
|
247
|
+
approvalTtlSec,
|
|
248
|
+
});
|
|
249
|
+
console.log(JSON.stringify(r, null, 2));
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case 'search': {
|
|
253
|
+
const q = args.join(' ');
|
|
254
|
+
const r = await ap.memory.search({ query: q, k: 10, includePinned: true });
|
|
255
|
+
console.log(JSON.stringify(r, null, 2));
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case 'ask': {
|
|
259
|
+
const r = await ap.mobile.ask({ prompt: args.join(' ') });
|
|
260
|
+
console.log(JSON.stringify(r, null, 2));
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case 'memory': {
|
|
264
|
+
const [sub, ...rest] = args;
|
|
265
|
+
if (sub === 'list') {
|
|
266
|
+
const r = await ap.memory.list({});
|
|
267
|
+
console.log(JSON.stringify(r, null, 2));
|
|
268
|
+
}
|
|
269
|
+
else if (sub === 'read') {
|
|
270
|
+
const [name] = rest;
|
|
271
|
+
if (!name)
|
|
272
|
+
throw new Error('usage: ujex memory read <name>');
|
|
273
|
+
const r = await ap.memory.read({ name });
|
|
274
|
+
console.log(JSON.stringify(r, null, 2));
|
|
275
|
+
}
|
|
276
|
+
else if (sub === 'write' || sub === 'append') {
|
|
277
|
+
const [name, ...content] = rest;
|
|
278
|
+
if (!name || content.length === 0)
|
|
279
|
+
throw new Error(`usage: ujex memory ${sub} <name> <content>`);
|
|
280
|
+
const input = { name, content: content.join(' ') };
|
|
281
|
+
const r = sub === 'write'
|
|
282
|
+
? await ap.memory.write(input)
|
|
283
|
+
: await ap.memory.append(input);
|
|
284
|
+
console.log(JSON.stringify(r, null, 2));
|
|
285
|
+
}
|
|
286
|
+
else if (sub === 'search') {
|
|
287
|
+
const query = rest.join(' ');
|
|
288
|
+
if (!query)
|
|
289
|
+
throw new Error('usage: ujex memory search <query>');
|
|
290
|
+
const r = await ap.memory.search({ query, k: 10, includePinned: true });
|
|
291
|
+
console.log(JSON.stringify(r, null, 2));
|
|
292
|
+
}
|
|
293
|
+
else if (sub === 'index') {
|
|
294
|
+
const r = await ap.memory.index({});
|
|
295
|
+
console.log(r.markdown);
|
|
296
|
+
}
|
|
297
|
+
else
|
|
298
|
+
throw new Error('memory list|read|write|append|search|index');
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case 'updateIp': {
|
|
302
|
+
const [fqdn, ip] = args;
|
|
303
|
+
const r = await ap.gateway.updateIp({ fqdn, ip });
|
|
304
|
+
console.log(JSON.stringify(r, null, 2));
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case 'issue': {
|
|
308
|
+
const [fqdn, ...rest] = args;
|
|
309
|
+
const staging = rest.includes('--staging');
|
|
310
|
+
const r = await ap.gateway.issue({ fqdn, staging });
|
|
311
|
+
console.log(JSON.stringify(r, null, 2));
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case 'secret': {
|
|
315
|
+
const [sub, name, value] = args;
|
|
316
|
+
if (sub === 'set') {
|
|
317
|
+
const c = loadConfig();
|
|
318
|
+
await ap.secrets.store({ agentId: c.agentId, name, value });
|
|
319
|
+
console.log('ok');
|
|
320
|
+
}
|
|
321
|
+
else if (sub === 'get') {
|
|
322
|
+
const r = await ap.secrets.get({ name });
|
|
323
|
+
console.log(r.value);
|
|
324
|
+
}
|
|
325
|
+
else
|
|
326
|
+
throw new Error('secret set|get');
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
case 'scheduler': {
|
|
330
|
+
const [sub, ...rest] = args;
|
|
331
|
+
if (sub === 'create') {
|
|
332
|
+
const [name, cron, webhookUrl] = rest;
|
|
333
|
+
if (!name || !cron || !webhookUrl)
|
|
334
|
+
throw new Error('usage: ujex scheduler create <name> <cron> <webhookUrl> [--payload=<json>] [--max-attempts=N]');
|
|
335
|
+
const payload = parseJSONFlag(rest, '--payload=');
|
|
336
|
+
const maxAttemptsRaw = flagValue(rest, '--max-attempts=');
|
|
337
|
+
const maxAttempts = maxAttemptsRaw ? Number(maxAttemptsRaw) : undefined;
|
|
338
|
+
const r = await ap.scheduler.createJob({ name, cron, webhookUrl, payload, maxAttempts });
|
|
339
|
+
console.log(JSON.stringify(r, null, 2));
|
|
340
|
+
}
|
|
341
|
+
else if (sub === 'replay') {
|
|
342
|
+
const [agentId, jobName, runId] = rest;
|
|
343
|
+
if (!agentId || !jobName || !runId)
|
|
344
|
+
throw new Error('usage: ujex scheduler replay <agentId> <jobName> <runId>');
|
|
345
|
+
const r = await ap.scheduler.dlqReplay({ agentId, jobName, runId });
|
|
346
|
+
console.log(JSON.stringify(r, null, 2));
|
|
347
|
+
}
|
|
348
|
+
else
|
|
349
|
+
throw new Error('scheduler create|replay');
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case 'tools': {
|
|
353
|
+
const [sub, ...rest] = args;
|
|
354
|
+
if (sub === 'register') {
|
|
355
|
+
const [agentId, name, kind, url] = rest;
|
|
356
|
+
if (!agentId || !name || !kind || !url)
|
|
357
|
+
throw new Error('usage: ujex tools register <agentId> <name> <kind> <url> [--auth-token=<t>]');
|
|
358
|
+
if (kind !== 'mcp' && kind !== 'http')
|
|
359
|
+
throw new Error('kind must be mcp or http');
|
|
360
|
+
const authToken = flagValue(rest, '--auth-token=');
|
|
361
|
+
const connectionId = flagValue(rest, '--connection-id=');
|
|
362
|
+
const r = await ap.tools.register({ agentId, name, kind, url, authToken, connectionId });
|
|
363
|
+
console.log(JSON.stringify(r, null, 2));
|
|
364
|
+
}
|
|
365
|
+
else if (sub === 'list') {
|
|
366
|
+
const r = await ap.tools.list({});
|
|
367
|
+
console.log(JSON.stringify(r, null, 2));
|
|
368
|
+
}
|
|
369
|
+
else if (sub === 'invoke') {
|
|
370
|
+
const [name] = rest;
|
|
371
|
+
if (!name)
|
|
372
|
+
throw new Error('usage: ujex tools invoke <name> [--args=<json>] [--timeout-ms=N]');
|
|
373
|
+
const toolArgs = parseJSONFlag(rest, '--args=');
|
|
374
|
+
const tmRaw = flagValue(rest, '--timeout-ms=');
|
|
375
|
+
const timeoutMs = tmRaw ? Number(tmRaw) : undefined;
|
|
376
|
+
const r = await ap.tools.invoke({ name, args: toolArgs, timeoutMs });
|
|
377
|
+
console.log(JSON.stringify(r, null, 2));
|
|
378
|
+
}
|
|
379
|
+
else if (sub === 'cred') {
|
|
380
|
+
const [name] = rest;
|
|
381
|
+
if (!name)
|
|
382
|
+
throw new Error('usage: ujex tools cred <name>');
|
|
383
|
+
const r = await ap.tools.getCredential({ name });
|
|
384
|
+
console.log(r.token ?? '');
|
|
385
|
+
}
|
|
386
|
+
else if (sub === 'connection-register') {
|
|
387
|
+
const [agentId, provider] = rest;
|
|
388
|
+
if (!agentId || !provider)
|
|
389
|
+
throw new Error('usage: ujex tools connection-register <agentId> <provider> [--scopes=a,b] [--access-token=<t>] [--refresh-token=<t>]');
|
|
390
|
+
const scopes = flagValue(rest, '--scopes=')?.split(',').map((s) => s.trim()).filter(Boolean);
|
|
391
|
+
const accessToken = flagValue(rest, '--access-token=');
|
|
392
|
+
const refreshToken = flagValue(rest, '--refresh-token=');
|
|
393
|
+
const r = await ap.tools.registerConnection({ agentId, provider, scopes, accessToken, refreshToken });
|
|
394
|
+
console.log(JSON.stringify(r, null, 2));
|
|
395
|
+
}
|
|
396
|
+
else if (sub === 'connections') {
|
|
397
|
+
const [agentId] = rest;
|
|
398
|
+
const target = agentId ?? loadConfig().agentId;
|
|
399
|
+
const r = await ap.tools.listConnections({ agentId: target });
|
|
400
|
+
console.log(JSON.stringify(r, null, 2));
|
|
401
|
+
}
|
|
402
|
+
else if (sub === 'revoke-connection') {
|
|
403
|
+
const [agentId, connectionId] = rest;
|
|
404
|
+
if (!agentId || !connectionId)
|
|
405
|
+
throw new Error('usage: ujex tools revoke-connection <agentId> <connectionId>');
|
|
406
|
+
const r = await ap.tools.revokeConnection({ agentId, connectionId });
|
|
407
|
+
console.log(JSON.stringify(r, null, 2));
|
|
408
|
+
}
|
|
409
|
+
else
|
|
410
|
+
throw new Error('tools register|list|invoke|cred|connection-register|connections|revoke-connection');
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
case 'gateway': {
|
|
414
|
+
const [sub, ...rest] = args;
|
|
415
|
+
if (sub === 'claim') {
|
|
416
|
+
const [fqdn] = rest;
|
|
417
|
+
const modeRaw = flagValue(rest, '--mode=');
|
|
418
|
+
const mode = modeRaw === 'duckdns' || modeRaw === 'custom' ? modeRaw : undefined;
|
|
419
|
+
const r = await ap.gateway.claimHostname({ fqdn, mode });
|
|
420
|
+
console.log(JSON.stringify(r, null, 2));
|
|
421
|
+
}
|
|
422
|
+
else if (sub === 'updateIp') {
|
|
423
|
+
const [fqdn, ip] = rest;
|
|
424
|
+
const r = await ap.gateway.updateIp({ fqdn, ip });
|
|
425
|
+
console.log(JSON.stringify(r, null, 2));
|
|
426
|
+
}
|
|
427
|
+
else if (sub === 'issue') {
|
|
428
|
+
const [fqdn] = rest;
|
|
429
|
+
const staging = rest.includes('--staging');
|
|
430
|
+
const r = await ap.gateway.issue({ fqdn, staging });
|
|
431
|
+
console.log(JSON.stringify(r, null, 2));
|
|
432
|
+
}
|
|
433
|
+
else if (sub === 'acme-start') {
|
|
434
|
+
const [fqdn] = rest;
|
|
435
|
+
const staging = rest.includes('--staging');
|
|
436
|
+
const r = await ap.gateway.acmeStart({ fqdn, staging });
|
|
437
|
+
console.log(JSON.stringify(r, null, 2));
|
|
438
|
+
}
|
|
439
|
+
else if (sub === 'acme-finish') {
|
|
440
|
+
const [orderId] = rest;
|
|
441
|
+
if (!orderId)
|
|
442
|
+
throw new Error('usage: ujex gateway acme-finish <orderId>');
|
|
443
|
+
const r = await ap.gateway.acmeFinish({ orderId });
|
|
444
|
+
console.log(JSON.stringify(r, null, 2));
|
|
445
|
+
}
|
|
446
|
+
else if (sub === 'duckdns-token') {
|
|
447
|
+
const [token] = rest;
|
|
448
|
+
if (!token)
|
|
449
|
+
throw new Error('usage: ujex gateway duckdns-token <token>');
|
|
450
|
+
const r = await ap.gateway.setDuckDnsToken({ token });
|
|
451
|
+
console.log(JSON.stringify(r, null, 2));
|
|
452
|
+
}
|
|
453
|
+
else if (sub === 'get-cert') {
|
|
454
|
+
const [fqdn] = rest;
|
|
455
|
+
const id = flagValue(rest, '--id=');
|
|
456
|
+
const r = await ap.gateway.getCert({ fqdn, id });
|
|
457
|
+
console.log(JSON.stringify(r, null, 2));
|
|
458
|
+
}
|
|
459
|
+
else
|
|
460
|
+
throw new Error('gateway claim|updateIp|issue|acme-start|acme-finish|duckdns-token|get-cert');
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
case 'audit': {
|
|
464
|
+
const [sub] = args;
|
|
465
|
+
if (sub === 'verify') {
|
|
466
|
+
const r = await ap.audit?.verify?.();
|
|
467
|
+
console.log(JSON.stringify(r ?? { note: 'live-chain verify is an HTTPS callable; for OFFLINE bundle verification run: ujex audit verify <dir|records.jsonl>' }, null, 2));
|
|
468
|
+
}
|
|
469
|
+
else if (sub === 'export-aat') {
|
|
470
|
+
const from = flagValue(args, '--from=');
|
|
471
|
+
const to = flagValue(args, '--to=');
|
|
472
|
+
const out = flagValue(args, '--out=');
|
|
473
|
+
if (!from || !to)
|
|
474
|
+
throw new Error('usage: ujex audit export-aat --from=ISO --to=ISO [--out=DIR] [--sign-manifest]');
|
|
475
|
+
const r = await ap.raw.call('auditExportAATBundle', { from, to, signManifest: hasFlag(args, '--sign-manifest') });
|
|
476
|
+
if (out && r && typeof r === 'object') {
|
|
477
|
+
const data = r;
|
|
478
|
+
if (out.endsWith('.json')) {
|
|
479
|
+
writeFileSync(out, JSON.stringify(data, null, 2));
|
|
480
|
+
console.log(`wrote ${out}`);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
mkdirSync(out, { recursive: true });
|
|
484
|
+
writeFileSync(join(out, 'records.jsonl'), data.records_jsonl ?? '');
|
|
485
|
+
writeFileSync(join(out, 'manifest.json'), JSON.stringify(data.manifest, null, 2));
|
|
486
|
+
if (data.manifest_sig)
|
|
487
|
+
writeFileSync(join(out, 'manifest.sig'), JSON.stringify(data.manifest_sig, null, 2));
|
|
488
|
+
writeFileSync(join(out, 'trust-bundle.txt'), data.trust_bundle_txt ?? '');
|
|
489
|
+
console.log(`wrote ${out}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
console.log(JSON.stringify(r ?? { note: 'callable auditExportAATBundle; invoke via SDK raw' }, null, 2));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
else if (sub === 'export-iso42005') {
|
|
497
|
+
const from = flagValue(args, '--from=');
|
|
498
|
+
const to = flagValue(args, '--to=');
|
|
499
|
+
if (!from || !to)
|
|
500
|
+
throw new Error('usage: ujex audit export-iso42005 --from=ISO --to=ISO');
|
|
501
|
+
const r = await ap.raw
|
|
502
|
+
?.call?.('auditExportISO42005', { from, to });
|
|
503
|
+
console.log(JSON.stringify(r ?? { note: 'invoke auditExportISO42005 via SDK raw' }, null, 2));
|
|
504
|
+
}
|
|
505
|
+
else if (sub === 'list') {
|
|
506
|
+
const limit = flagValue(args, '--limit=');
|
|
507
|
+
const before = flagValue(args, '--before=');
|
|
508
|
+
const r = await ap.raw
|
|
509
|
+
?.call?.('listMyAuditEntries', { limit: limit ? Number(limit) : 50, beforeSeq: before ? Number(before) : null });
|
|
510
|
+
console.log(JSON.stringify(r ?? { note: 'invoke listMyAuditEntries via SDK raw' }, null, 2));
|
|
511
|
+
}
|
|
512
|
+
else
|
|
513
|
+
throw new Error('audit verify|export-aat|export-iso42005|list');
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
case 'identity': {
|
|
517
|
+
const [sub, agentId] = args;
|
|
518
|
+
if (sub === 'passport') {
|
|
519
|
+
if (!agentId)
|
|
520
|
+
throw new Error('usage: ujex identity passport <agentId>');
|
|
521
|
+
const r = await ap.identity.passport({ agentId });
|
|
522
|
+
console.log(JSON.stringify(r, null, 2));
|
|
523
|
+
}
|
|
524
|
+
else
|
|
525
|
+
throw new Error('identity passport');
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
case 'import': {
|
|
529
|
+
const [source, file] = args;
|
|
530
|
+
const sources = ['mem0', 'zep', 'letta'];
|
|
531
|
+
if (!source || !file || !sources.includes(source)) {
|
|
532
|
+
throw new Error(`usage: ujex import (${sources.join('|')}) <file.json>`);
|
|
533
|
+
}
|
|
534
|
+
const r = await importFile(source, file, async (name, data) => {
|
|
535
|
+
return ap.raw?.call?.(name, data);
|
|
536
|
+
});
|
|
537
|
+
console.log(JSON.stringify(r, null, 2));
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
case 'memory-beads-export': {
|
|
541
|
+
const [out] = args;
|
|
542
|
+
if (!out)
|
|
543
|
+
throw new Error('usage: ujex memory-beads-export <out.jsonl>');
|
|
544
|
+
const list = await ap.memory.list({});
|
|
545
|
+
const memories = (list.items ?? [])
|
|
546
|
+
.map((m) => ({ name: m.name, content: m.content ?? '', createdAt: m.createdAt, updatedAt: m.updatedAt }));
|
|
547
|
+
const r = exportToBeadsJsonl(memories, out);
|
|
548
|
+
console.log(`exported ${r.count} rows to ${out}`);
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
case 'memory-beads-import': {
|
|
552
|
+
const [inp] = args;
|
|
553
|
+
if (!inp)
|
|
554
|
+
throw new Error('usage: ujex memory-beads-import <in.jsonl>');
|
|
555
|
+
const rows = importFromBeadsJsonl(inp);
|
|
556
|
+
let imported = 0;
|
|
557
|
+
for (const row of rows) {
|
|
558
|
+
const w = rowToUjexMemoryWrite(row);
|
|
559
|
+
try {
|
|
560
|
+
await ap.memory.write(w);
|
|
561
|
+
imported++;
|
|
562
|
+
}
|
|
563
|
+
catch (e) {
|
|
564
|
+
console.warn(`skipped ${row.name}:`, e instanceof Error ? e.message : e);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
console.log(`imported ${imported}/${rows.length} memories`);
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
default:
|
|
571
|
+
console.error(`unknown command: ${cmd}\n${HELP}`);
|
|
572
|
+
process.exit(2);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
main().catch((e) => { console.error(e.message || e); process.exit(1); });
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ujexdev/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command-line client for Ujex Postbox, approvals, Recall/Memory, Gateway, tools, and audit exports.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Ujex <hello@ujex.dev>",
|
|
7
|
+
"homepage": "https://ujex.dev",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"ujex",
|
|
10
|
+
"postbox",
|
|
11
|
+
"agents",
|
|
12
|
+
"audit",
|
|
13
|
+
"memory",
|
|
14
|
+
"cli"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"ujex": "dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc && chmod +x dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@ujexdev/audit-chain": "^0.2.0",
|
|
34
|
+
"@ujexdev/client": "^0.1.0",
|
|
35
|
+
"firebase": "^12.13.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20",
|
|
39
|
+
"typescript": "^5.5.4"
|
|
40
|
+
}
|
|
41
|
+
}
|