@josfox/jos 4.0.8 → 5.0.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 +208 -21
- package/PROMISES.md +52 -0
- package/README.md +294 -146
- package/SECURITY-MCP.md +117 -0
- package/SECURITY-SECRETS.md +108 -0
- package/STYLE.md +23 -0
- package/bin/jos +139 -67
- package/bin/jos.cmd +5 -0
- package/examples/01-hello.jos +44 -0
- package/examples/02-criteria.jos +54 -0
- package/examples/03-flow-gate.jos +83 -0
- package/examples/04-use-template/README.md +22 -0
- package/examples/04-use-template/lib/greeting.jos +56 -0
- package/examples/04-use-template/use.jos +64 -0
- package/examples/05-sealed/.jos/manifest.jos +53 -0
- package/examples/05-sealed/README.md +25 -0
- package/examples/06-requires.jos +42 -0
- package/examples/07-sandbox.jos +39 -0
- package/examples/08-ids-ledger-meter.md +26 -0
- package/examples/10-ollama.jos +47 -0
- package/examples/11-openai.jos +47 -0
- package/examples/12-gemini.jos +47 -0
- package/examples/13-anthropic.jos +47 -0
- package/examples/14-serve.jos +88 -0
- package/examples/README.md +105 -0
- package/examples/hello.jos +47 -0
- package/package.json +31 -9
- package/promises.json +894 -0
- package/src/args.js +47 -0
- package/src/commands/_frozen/add.README.md +1 -0
- package/src/commands/_frozen/get.README.md +1 -0
- package/src/commands/_frozen/llm.README.md +1 -0
- package/src/commands/_frozen/prompts.README.md +1 -0
- package/src/commands/_frozen/repo.README.md +1 -0
- package/src/commands/_frozen/serve-explorer.README.md +1 -0
- package/src/commands/ids.js +101 -0
- package/src/commands/init.js +113 -147
- package/src/commands/ledger.js +94 -0
- package/src/commands/meter.js +96 -0
- package/src/commands/migrate.js +214 -0
- package/src/commands/overview.js +118 -0
- package/src/commands/run.js +164 -214
- package/src/commands/runs.js +102 -0
- package/src/commands/secrets.js +695 -115
- package/src/commands/start.js +210 -0
- package/src/commands/validate.js +153 -165
- package/src/commands/verify.js +56 -0
- package/src/commands/version.js +23 -0
- package/src/intention.js +114 -0
- package/src/kernel.js +69 -0
- package/src/ledgers.js +57 -0
- package/src/mcp-guide.js +138 -0
- package/src/mcp.js +734 -0
- package/src/serve.js +38 -770
- package/src/ui.js +98 -0
- package/src/commands/add.js +0 -145
- package/src/commands/get.js +0 -245
- package/src/commands/repo.js +0 -139
- package/src/index.js +0 -0
package/src/intention.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// The intention of a document and the verdict of a run, in words, for `jos run` and `jos validate` (text mode only;
|
|
3
|
+
// `--json` is the kernel's result unchanged). The intention comes from the document as jos-core normalized it
|
|
4
|
+
// (intention.objective, intention.success_criteria, guardrails.avoid and guardrails.limit); the verdict comes only
|
|
5
|
+
// from the receipts the kernel returned (criteria[].check / verdict / detail): the host evaluates nothing itself.
|
|
6
|
+
|
|
7
|
+
const ONE_LINE = 160;
|
|
8
|
+
|
|
9
|
+
const isObj = (x) => x !== null && typeof x === 'object' && !Array.isArray(x);
|
|
10
|
+
const q = (s) => JSON.stringify(s);
|
|
11
|
+
|
|
12
|
+
function oneLine(s) {
|
|
13
|
+
const t = s.replace(/\s+/g, ' ').trim();
|
|
14
|
+
return t.length > ONE_LINE ? t.slice(0, ONE_LINE - 1) + '…' : t;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// A declared success criterion as a short phrase: "exit 0", 'stdout contains "x"', "all of (exit 0; stderr is empty)".
|
|
18
|
+
// Criteria come from a document jos-core validated, so each object has the one key its schema allows.
|
|
19
|
+
const short = (h) => h.replace(/^sha256:/, '').slice(0, 12) + '…';
|
|
20
|
+
function describe(c) {
|
|
21
|
+
if (typeof c === 'string') return oneLine(c) + ' (descriptive)';
|
|
22
|
+
const k = Object.keys(c)[0];
|
|
23
|
+
const v = c[k];
|
|
24
|
+
switch (k) {
|
|
25
|
+
case 'exit': return 'exit ' + v;
|
|
26
|
+
case 'stdout_contains': return 'stdout contains ' + q(v);
|
|
27
|
+
case 'stdout_matches': return 'stdout matches /' + v + '/';
|
|
28
|
+
case 'stderr_empty': return 'stderr is empty';
|
|
29
|
+
case 'file_exists': return 'file ' + q(v) + ' exists';
|
|
30
|
+
case 'file_sha256': return 'file ' + q(v.path) + ' has sha256 ' + short(v.hash);
|
|
31
|
+
case 'sha256': return 'output has sha256 ' + short(v);
|
|
32
|
+
case 'json_schema': return 'output validates against a JSON Schema';
|
|
33
|
+
case 'judge': return 'a judge engine grades it: ' + oneLine(v.rubric);
|
|
34
|
+
case 'all': case 'any': return k + ' of (' + v.map(describe).join('; ') + ')';
|
|
35
|
+
case 'not': return 'not (' + describe(v) + ')';
|
|
36
|
+
default: return k + ' ' + q(v);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const LIMIT_WORDS = { seconds: ' s', output_bytes: ' output bytes per stream', tokens: ' tokens', retries: ' retries' };
|
|
41
|
+
function limitText(limit) {
|
|
42
|
+
return Object.keys(limit).map((k) => {
|
|
43
|
+
if (k === 'cost_usd') return limit[k] + ' USD';
|
|
44
|
+
if (Object.prototype.hasOwnProperty.call(LIMIT_WORDS, k)) return limit[k] + LIMIT_WORDS[k];
|
|
45
|
+
return k + ' ' + q(limit[k]);
|
|
46
|
+
}).join(', ');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// "Intention:", "Done when:", and when declared "Avoid:" and "Limit:", for the normalized document `doc`.
|
|
50
|
+
function intention(p, doc) {
|
|
51
|
+
const { C } = p;
|
|
52
|
+
const it = isObj(doc.intention) ? doc.intention : {};
|
|
53
|
+
const g = isObj(doc.guardrails) ? doc.guardrails : {};
|
|
54
|
+
p.line();
|
|
55
|
+
p.line(`${C.bold}Intention:${C.reset} ` + (typeof it.objective === 'string' && it.objective.trim() ? oneLine(it.objective) : `${C.gray}not declared${C.reset}`));
|
|
56
|
+
const criteria = it.success_criteria === undefined ? [] : [].concat(it.success_criteria);
|
|
57
|
+
if (!criteria.length) p.line(`${C.bold}Done when:${C.reset} ${C.gray}not declared${C.reset}`);
|
|
58
|
+
else {
|
|
59
|
+
p.line(`${C.bold}Done when:${C.reset}`);
|
|
60
|
+
for (const c of criteria) p.item(describe(c));
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(g.avoid) && g.avoid.length) p.line(`${C.bold}Avoid:${C.reset} ` + g.avoid.map(q).join(', '));
|
|
63
|
+
if (isObj(g.limit) && Object.keys(g.limit).length) p.line(`${C.bold}Limit:${C.reset} ` + limitText(g.limit));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// "Inferred:": the `inferred` section of jos-core (2.0.0-rc.8) for a valid document, one line per JSON Pointer in the
|
|
67
|
+
// kernel's order: what the kernel applies that the document does not write (value and source, SPEC section or jos-core
|
|
68
|
+
// default, and its note), and, flagged with ⚠, what the document does not declare.
|
|
69
|
+
function inferred(p, section) {
|
|
70
|
+
const { C } = p;
|
|
71
|
+
const keys = Object.keys(section);
|
|
72
|
+
p.line();
|
|
73
|
+
if (!keys.length) {
|
|
74
|
+
p.line(`${C.bold}Inferred:${C.reset} ${C.gray}nothing (the document writes every member jos-core applies)${C.reset}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
p.line(`${C.bold}Inferred:${C.reset} ${C.gray}(applied by jos-core, not written in the document)${C.reset}`);
|
|
78
|
+
for (const k of keys) {
|
|
79
|
+
const e = section[k];
|
|
80
|
+
const note = e.note ? ` ${C.gray}— ${oneLine(e.note)}${C.reset}` : '';
|
|
81
|
+
if (e.source === 'not declared') p.line(` ${C.yellow}⚠ ${k}: not declared${C.reset}` + note);
|
|
82
|
+
else p.item(k + (Object.prototype.hasOwnProperty.call(e, 'value') ? ' = ' + oneLine(JSON.stringify(e.value)) : '') + ` ${C.gray}(${e.source})${C.reset}` + note);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// One line per criterion verdict of a receipt, indented by depth; combinators list their items below them.
|
|
87
|
+
function criteriaLines(p, list, depth) {
|
|
88
|
+
const { C } = p;
|
|
89
|
+
const pad = ' '.repeat(depth);
|
|
90
|
+
for (const c of list) {
|
|
91
|
+
const sign = c.verdict === 'pass' ? `${C.green}✓` : c.verdict === 'fail' ? `${C.red}✖` : `${C.gray}•`;
|
|
92
|
+
const note = c.verdict === 'descriptive' ? ' (descriptive, recorded only)' : '';
|
|
93
|
+
p.line(`${pad}${sign} ${c.check}${C.reset}${C.gray}: ${c.detail}${note}${C.reset}`);
|
|
94
|
+
if (Array.isArray(c.items)) criteriaLines(p, c.items, depth + 1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// "Verdict:": per receipt (each step of a flow, then the flow itself) its code and the verdict of each criterion.
|
|
99
|
+
// The caller passes a result with at least one receipt.
|
|
100
|
+
function verdict(p, result) {
|
|
101
|
+
const { C } = p;
|
|
102
|
+
p.line();
|
|
103
|
+
p.line(`${C.bold}Verdict:${C.reset}`);
|
|
104
|
+
for (const r of result.receipts) {
|
|
105
|
+
const mark = r.ok ? `${C.green}✓` : `${C.red}✖`;
|
|
106
|
+
const label = r.flow ? r.step + ' (flow ' + r.flow + ')' : r.step;
|
|
107
|
+
p.line(` ${C.blue}[${r.attempt}] ${label}${C.reset} ${mark} code ${r.code}${C.reset}` + (r.reason ? ` ${C.gray}${r.reason}${C.reset}` : ''));
|
|
108
|
+
const list = r.criteria || [];
|
|
109
|
+
if (list.length) criteriaLines(p, list, 2);
|
|
110
|
+
else p.line(` ${C.gray}no criterion evaluated${C.reset}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { describe, limitText, oneLine, intention, inferred, verdict };
|
package/src/kernel.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Locates the jos-core reference kernel. No network.
|
|
3
|
+
// 1. the installed dependency @josfox/jos-core
|
|
4
|
+
// 2. ../../../kernel/js/jos-core.js (monorepo checkout)
|
|
5
|
+
// 3. otherwise a clear error; hosts map it to exit code 5 (SPEC-2.0 section 7.1).
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const ui = require('./ui');
|
|
10
|
+
|
|
11
|
+
const MONOREPO_PATH = path.resolve(__dirname, '..', '..', '..', 'kernel', 'js', 'jos-core.js');
|
|
12
|
+
|
|
13
|
+
class KernelNotFound extends Error {}
|
|
14
|
+
|
|
15
|
+
let cached = null;
|
|
16
|
+
|
|
17
|
+
function locate() {
|
|
18
|
+
if (cached) return cached;
|
|
19
|
+
let file = null;
|
|
20
|
+
let source = null;
|
|
21
|
+
try {
|
|
22
|
+
file = require.resolve('@josfox/jos-core');
|
|
23
|
+
source = 'dependency';
|
|
24
|
+
} catch (e) {
|
|
25
|
+
if (e.code !== 'MODULE_NOT_FOUND') throw e;
|
|
26
|
+
}
|
|
27
|
+
if (!file && fs.existsSync(MONOREPO_PATH)) {
|
|
28
|
+
file = MONOREPO_PATH;
|
|
29
|
+
source = 'monorepo';
|
|
30
|
+
}
|
|
31
|
+
if (!file) {
|
|
32
|
+
throw new KernelNotFound(
|
|
33
|
+
'jos-core not found. Install @josfox/jos-core next to @josfox/jos, ' +
|
|
34
|
+
'or run from a checkout that contains kernel/js/jos-core.js.');
|
|
35
|
+
}
|
|
36
|
+
let core;
|
|
37
|
+
try {
|
|
38
|
+
core = require(file);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
throw new KernelNotFound('jos-core at ' + file + ' failed to load: ' + e.message);
|
|
41
|
+
}
|
|
42
|
+
for (const fn of ['validate', 'run', 'loadFile']) {
|
|
43
|
+
if (typeof core[fn] !== 'function') {
|
|
44
|
+
throw new KernelNotFound('jos-core at ' + file + ' does not export ' + fn + '()');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
cached = { core, version: core.VERSION, file, source };
|
|
48
|
+
return cached;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The kernel's core when it has every function in names ('validate', 'ledger.verify', ...); otherwise KernelNotFound
|
|
52
|
+
// naming the first one missing (an older jos-core), which a command turns into exit 5 with fail().
|
|
53
|
+
function need(k, names) {
|
|
54
|
+
for (const n of names) {
|
|
55
|
+
let v = k.core;
|
|
56
|
+
for (const part of n.split('.')) v = v === null || v === undefined ? undefined : v[part];
|
|
57
|
+
if (typeof v !== 'function') throw new KernelNotFound('jos-core ' + k.version + ' at ' + k.file + ' does not export ' + n + '()');
|
|
58
|
+
}
|
|
59
|
+
return k.core;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Prints the error and returns exit code 5, for use as `return kernel.fail(e)`.
|
|
63
|
+
function fail(e) {
|
|
64
|
+
if (!(e instanceof KernelNotFound)) throw e;
|
|
65
|
+
ui.printer(process.stderr).fail('jos: ' + e.message);
|
|
66
|
+
return 5;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { locate, need, fail, KernelNotFound, MONOREPO_PATH };
|
package/src/ledgers.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Run ledgers for `jos ledger verify`, `jos meter` and `jos runs`: thin calls into the ledger API of jos-core
|
|
3
|
+
// (Ledger 0.1.0: find, read, verify, meter). Read-only: nothing here writes, creates ~/.jos or runs a document.
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
// A run id as jos-core mints it (`run_` + a ULID); anything else must be the path of a ledger.
|
|
8
|
+
const RUN_ID = /^run_[0-9A-Za-z]{1,64}$/;
|
|
9
|
+
|
|
10
|
+
const isFile = (p) => { try { return fs.statSync(p).isFile(); } catch (e) { return false; } };
|
|
11
|
+
const isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch (e) { return false; } };
|
|
12
|
+
|
|
13
|
+
// Where the ledger of `ref` is: { file } for a ledger file, a run folder holding ledger.jsonl, or a run id that
|
|
14
|
+
// jos-core finds (.jos/replays/ of the folders above the current directory, then ~/.jos/runs/); { error: 'usage' }
|
|
15
|
+
// when ref is neither a path nor a run id; { error: 'missing' } when no ledger of that run exists.
|
|
16
|
+
function locate(core, ref, josHome) {
|
|
17
|
+
if (isFile(ref)) return { file: path.resolve(ref) };
|
|
18
|
+
if (isDir(ref) && isFile(path.join(ref, 'ledger.jsonl'))) return { file: path.resolve(ref, 'ledger.jsonl') };
|
|
19
|
+
if (!RUN_ID.test(ref)) return { error: 'usage' };
|
|
20
|
+
const file = core.ledger.find(ref, { home: path.dirname(josHome) });
|
|
21
|
+
return file === null ? { error: 'missing' } : { file };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The lines of a ledger file, or { errors } when it cannot be read (a JosError of the kernel).
|
|
25
|
+
function read(core, file) {
|
|
26
|
+
try { return { lines: core.ledger.read(file) }; } catch (e) {
|
|
27
|
+
if (!(e instanceof core.JosError)) throw e;
|
|
28
|
+
return { errors: e.errors };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// What a listing shows of one run, from its ledger lines: whether the ledger verifies and, only when it does, when the
|
|
33
|
+
// run started, the document (the name on its top-level receipt, the last one, and the art_ it admitted), its exit code
|
|
34
|
+
// and its metered cost (null when no engine reported one). A ledger that does not verify says only why.
|
|
35
|
+
const str = (x) => (typeof x === 'string' ? x : null);
|
|
36
|
+
const obj = (x) => (x !== null && typeof x === 'object' ? x : {});
|
|
37
|
+
function summary(core, lines) {
|
|
38
|
+
const v = core.ledger.verify(lines, {});
|
|
39
|
+
if (!v.ok) return { ledger_ok: false, reason: v.reason, started: null, document: null, artifact: null, code: null, cost_usd: null };
|
|
40
|
+
const entries = lines.map((l) => JSON.parse(l));
|
|
41
|
+
const m = core.ledger.meter(lines);
|
|
42
|
+
const receipts = entries.filter((e) => e.kind === 'receipt');
|
|
43
|
+
const top = receipts.length ? obj(receipts[receipts.length - 1].body) : {};
|
|
44
|
+
return {
|
|
45
|
+
ledger_ok: true, reason: null, started: str(entries[0].ts), document: str(top.step),
|
|
46
|
+
artifact: str(obj(entries[entries.length - 1].body).artifact), code: typeof m.code === 'number' ? m.code : null, cost_usd: m.cost_usd,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The text after a failed lookup, for the printer of a command.
|
|
51
|
+
function explainMissing(err, cmd, ref, josHome) {
|
|
52
|
+
err.fail('jos ' + cmd + ': no ledger for ' + ref);
|
|
53
|
+
err.hint('searched .jos/replays/ of the folders above ' + process.cwd() + ' and ' + path.join(josHome, 'runs'));
|
|
54
|
+
err.hint('list the runs with: jos runs');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { RUN_ID, locate, read, summary, explainMissing };
|
package/src/mcp-guide.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a person reads about `jos serve --mcp`: the help screen (`jos serve --help`, stdout) and the notice the server
|
|
3
|
+
* prints on stderr when its stdin is a terminal (a person, not an MCP client). Both name the tools with one line each,
|
|
4
|
+
* what a client cannot do, and how to connect this very bin/jos, with its absolute path resolved at run time.
|
|
5
|
+
* Nothing here writes to the JSON-RPC channel; every line goes through a printer of ui.js (STYLE.md).
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const ui = require('./ui');
|
|
12
|
+
|
|
13
|
+
const BIN = path.resolve(__dirname, '..', 'bin', 'jos');
|
|
14
|
+
const SECURITY = path.resolve(__dirname, '..', 'SECURITY-MCP.md');
|
|
15
|
+
|
|
16
|
+
// One line per tool of src/mcp.js (test/mcp.test.js checks that every tool has one and that no extra one is listed).
|
|
17
|
+
const USES = {
|
|
18
|
+
list_files: 'list the files and folders of a directory under the root (recursive to depth 3)',
|
|
19
|
+
read_file: 'read a regular file under the root (at most 10 MB)',
|
|
20
|
+
search_files: 'find files under the root by a glob on the name (* and ?; 100 matches, depth 5)',
|
|
21
|
+
read_markdown: 'read a Markdown file under the root, with its line count',
|
|
22
|
+
list_modules: 'list the modules in ~/.jos/modules and whether ~/.jos/modules.json enables them',
|
|
23
|
+
list_processes: 'list the runs recorded in ~/.jos/runs/*/report.json',
|
|
24
|
+
hot_reload: 'read ~/.jos/modules.json again and report the enabled and disabled modules',
|
|
25
|
+
write_file: 'create or overwrite a file under the root, never under ~/.jos',
|
|
26
|
+
run_command: 'run a program with an argv (no shell) in a folder under the root; each one shown on stderr',
|
|
27
|
+
};
|
|
28
|
+
const FLAG = { write_file: '--allow-write', run_command: '--allow-shell' };
|
|
29
|
+
|
|
30
|
+
// What --allow-shell grants, said the same way in the help screen, the notice and SECURITY-MCP.md (review OAI-RC6-05).
|
|
31
|
+
const SHELL_RISK = '--allow-shell grants local code execution with your user privileges: a program it runs can read ' +
|
|
32
|
+
'your secrets (~/.jos/secrets, ~/.ssh, cloud keys) outside the MCP tools and their denials. Do not give it to an ' +
|
|
33
|
+
'untrusted agent without an external sandbox (a container or a virtual machine).';
|
|
34
|
+
|
|
35
|
+
const CANNOT = [
|
|
36
|
+
'reach a file outside the root (real paths, symlinks resolved, device and inode compared)',
|
|
37
|
+
'read, list, search or write ~/.jos/secrets, nor any .master or vault.json under ~/.jos',
|
|
38
|
+
'write a file without --allow-write, nor under ~/.jos with it',
|
|
39
|
+
'run a program without --allow-shell (with it the program runs as you and could read ~/.jos/secrets)',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// The absolute path of a `jos` on PATH that is this bin/jos (an npm install), or null.
|
|
43
|
+
function onPath(env) {
|
|
44
|
+
const self = fs.realpathSync(BIN);
|
|
45
|
+
for (const dir of String(env.PATH || '').split(path.delimiter)) {
|
|
46
|
+
if (!path.isAbsolute(dir)) continue;
|
|
47
|
+
const p = path.join(dir, 'jos');
|
|
48
|
+
try { if (fs.realpathSync(p) === self) return p; } catch (e) { /* not there */ }
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// How to connect this server, read-only, with `root` as its root: Claude Code, then Claude Desktop / Cursor.
|
|
54
|
+
function connect(p, root, env) {
|
|
55
|
+
const { C } = p;
|
|
56
|
+
const args = [BIN, 'serve', '--mcp', '--root', root];
|
|
57
|
+
p.line(`${C.bold}Connect it${C.reset} ${C.gray}(read-only, root ${root})${C.reset}`);
|
|
58
|
+
const home = os.homedir();
|
|
59
|
+
if (home === root || home.startsWith(root.endsWith(path.sep) ? root : root + path.sep)) {
|
|
60
|
+
p.warn('This root is your home directory or contains it: put a project folder after --root before you copy these.');
|
|
61
|
+
}
|
|
62
|
+
p.line(` ${C.white}Claude Code:${C.reset}`);
|
|
63
|
+
p.cmd('claude mcp add jos -- node ' + args.map(ui.shellArg).join(' '));
|
|
64
|
+
const installed = onPath(env);
|
|
65
|
+
if (installed) p.cmd('claude mcp add jos -- jos ' + args.slice(1).map(ui.shellArg).join(' ') + `${C.reset} ${C.gray}(jos on PATH: ${installed})`);
|
|
66
|
+
p.cmd('claude mcp remove jos' + `${C.reset} ${C.gray}(to remove it)`);
|
|
67
|
+
p.line(` ${C.white}Claude Desktop${C.reset} ${C.gray}(claude_desktop_config.json)${C.reset} ${C.white}or Cursor${C.reset} ${C.gray}(.cursor/mcp.json):${C.reset}`);
|
|
68
|
+
const block = JSON.stringify({ mcpServers: { jos: { command: 'node', args } } }, null, 2);
|
|
69
|
+
for (const l of block.split('\n')) p.line(' ' + l);
|
|
70
|
+
p.hint('If the client cannot find node, put its absolute path in "command": ' + process.execPath);
|
|
71
|
+
p.hint('Add --allow-write or --allow-shell to the arguments only if the client needs them (SECURITY-MCP.md).');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function tools(p, names, marked) {
|
|
75
|
+
const w = Math.max(...names.map((n) => n.length));
|
|
76
|
+
for (const n of names) {
|
|
77
|
+
p.item(`${p.C.cyan}${n.padEnd(w)}${p.C.reset} ${USES[n]}` + (marked && FLAG[n] ? ` ${p.C.yellow}(only with ${FLAG[n]})${p.C.reset}` : ''));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// `jos serve --help`. names: every tool of src/mcp.js, the gated ones included; root: the root it would serve.
|
|
82
|
+
function help(stream, names, root, env) {
|
|
83
|
+
const p = ui.printer(stream);
|
|
84
|
+
const { C } = p;
|
|
85
|
+
p.header('JOS SERVE', 'MCP server over stdio (Model Context Protocol)');
|
|
86
|
+
p.text(`${C.bold}Usage:${C.reset}
|
|
87
|
+
jos serve --mcp [--root <dir>] [--allow-write] [--allow-shell]
|
|
88
|
+
|
|
89
|
+
${C.bold}Options:${C.reset}
|
|
90
|
+
--mcp Start as MCP server (stdio JSON-RPC mode); read-only by default
|
|
91
|
+
--root <dir> The one directory the client may reach (default: the working
|
|
92
|
+
directory). Without it the server refuses to start in "/", in
|
|
93
|
+
your home directory or in any directory that contains it.
|
|
94
|
+
--allow-write Also offer write_file (inside the root, never under ~/.jos)
|
|
95
|
+
--allow-shell Also offer run_command: the client runs any program as you, with
|
|
96
|
+
an argv and no shell (announced on stderr; variables named like a
|
|
97
|
+
secret of the vault are dropped from its environment). This is
|
|
98
|
+
local code execution with your privileges: never for an
|
|
99
|
+
untrusted agent without an external sandbox
|
|
100
|
+
--help, -h Show this help
|
|
101
|
+
|
|
102
|
+
`);
|
|
103
|
+
p.line(`${C.bold}Tools an MCP client gets:${C.reset}`);
|
|
104
|
+
tools(p, names, true);
|
|
105
|
+
p.line();
|
|
106
|
+
p.line(`${C.bold}What an MCP client cannot do:${C.reset}`);
|
|
107
|
+
for (const c of CANNOT) p.item(c);
|
|
108
|
+
p.warn(SHELL_RISK);
|
|
109
|
+
p.hint('The module tools read ~/.jos/modules.json, ~/.jos/modules and ~/.jos/runs only.');
|
|
110
|
+
p.hint('Details: ' + SECURITY);
|
|
111
|
+
p.line();
|
|
112
|
+
connect(p, root, env);
|
|
113
|
+
p.line();
|
|
114
|
+
p.hint('The HTTP file explorer is disabled until F3.');
|
|
115
|
+
p.line();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The notice on stderr when stdin is a terminal. p: a printer on stderr; names: the tools this server offers.
|
|
119
|
+
function notice(p, context, names) {
|
|
120
|
+
const { C } = p;
|
|
121
|
+
p.header('JOS SERVE', 'MCP server on stdio, waiting for a client');
|
|
122
|
+
p.warn('This is an MCP server: it reads JSON-RPC 2.0 on stdin and answers on stdout.');
|
|
123
|
+
p.hint('An MCP client starts this command and talks to it; typing here is not needed.');
|
|
124
|
+
p.line();
|
|
125
|
+
p.field('Root', context.root);
|
|
126
|
+
const on = [context.allowWrite && 'write_file (--allow-write)', context.allowShell && 'run_command (--allow-shell)'].filter(Boolean);
|
|
127
|
+
p.field('Mode', on.length ? `read-only plus ${C.yellow}${on.join(', ')}${C.reset}` : 'read-only');
|
|
128
|
+
p.field('Denied', '~/.jos/secrets, and any .master or vault.json under ~/.jos');
|
|
129
|
+
if (context.allowShell) p.warn(SHELL_RISK);
|
|
130
|
+
p.line();
|
|
131
|
+
p.line(`${C.bold}Tools:${C.reset}`);
|
|
132
|
+
tools(p, names, false);
|
|
133
|
+
p.line();
|
|
134
|
+
connect(p, context.root, context.env);
|
|
135
|
+
p.line();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { help, notice, USES, BIN, onPath, SHELL_RISK };
|