skynet-graph 1.2.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/CONTRIBUTING.md +22 -0
- package/LICENSE +661 -0
- package/README.md +245 -0
- package/bin/sg +20 -0
- package/concepts/README.md +23 -0
- package/concepts/_substrate/Claim/Confidence/Trusted.json +6 -0
- package/concepts/_substrate/Claim/Confidence.json +10 -0
- package/concepts/_substrate/Claim/Freshness/Stale.json +7 -0
- package/concepts/_substrate/Claim/Freshness.json +5 -0
- package/concepts/_substrate/Claim/Verification/Refuted.json +6 -0
- package/concepts/_substrate/Claim/Verification.json +6 -0
- package/concepts/_substrate/Claim.json +4 -0
- package/concepts/_substrate/Frontier/InBeam.json +6 -0
- package/concepts/_substrate/Frontier/Stuck.json +6 -0
- package/concepts/_substrate/Frontier.json +5 -0
- package/concepts/_substrate/Intake/ToTask.json +22 -0
- package/concepts/_substrate/Intake/Typed.json +10 -0
- package/concepts/_substrate/Intake.json +27 -0
- package/concepts/_substrate/Task/Answer.json +12 -0
- package/concepts/_substrate/Task/Complexity/Atomic.json +5 -0
- package/concepts/_substrate/Task/Complexity/Compound/Expansion.json +7 -0
- package/concepts/_substrate/Task/Complexity/Compound.json +6 -0
- package/concepts/_substrate/Task/Complexity.json +6 -0
- package/concepts/_substrate/Task/EvalComplexity.json +13 -0
- package/concepts/_substrate/Task/ReportUp.json +6 -0
- package/concepts/_substrate/Task/Rollup.json +10 -0
- package/concepts/_substrate/Task.json +5 -0
- package/concepts/clinical/Diagnosis/Medication.json +6 -0
- package/concepts/clinical/Diagnosis.json +15 -0
- package/concepts/clinical/Observation/LabValue/OutOfRange.json +6 -0
- package/concepts/clinical/Observation/LabValue.json +5 -0
- package/concepts/clinical/Observation.json +6 -0
- package/concepts/common/Document/pathBasket.json +6 -0
- package/concepts/common/Document.json +3 -0
- package/concepts/common/Edge/Distance.json +13 -0
- package/concepts/common/Edge/Stay/LongStay.json +8 -0
- package/concepts/common/Edge/Stay.json +8 -0
- package/concepts/common/Edge/Travel/LongTravel.json +16 -0
- package/concepts/common/Edge/Travel/ShortTravel.json +11 -0
- package/concepts/common/Edge/Travel/targetNode.json +7 -0
- package/concepts/common/Edge/Travel.json +7 -0
- package/concepts/common/Edge.json +3 -0
- package/concepts/common/Vertice.json +3 -0
- package/concepts/supply/Fulfillment.json +10 -0
- package/concepts/supply/Inventory/Reorder.json +7 -0
- package/concepts/supply/Inventory.json +5 -0
- package/concepts/supply/Procurement/SupplierConfirm.json +6 -0
- package/concepts/supply/Procurement.json +8 -0
- package/concepts/supply/Transport.json +5 -0
- package/doc/API.md +556 -0
- package/doc/CAPABILITIES.md +373 -0
- package/doc/MODELISATION.md +511 -0
- package/doc/architecture.md +267 -0
- package/doc/concept-as-graph.md +379 -0
- package/doc/concept-learning.md +168 -0
- package/doc/creative-loop-map.md +202 -0
- package/doc/original-2016-doc.md +148 -0
- package/doc/usage.md +501 -0
- package/lib/authoring/abstract.js +401 -0
- package/lib/authoring/abstraction.js +190 -0
- package/lib/authoring/adapt.js +393 -0
- package/lib/authoring/ancestry.js +216 -0
- package/lib/authoring/author.js +147 -0
- package/lib/authoring/bounded-merge.js +57 -0
- package/lib/authoring/canon.js +158 -0
- package/lib/authoring/clock.js +123 -0
- package/lib/authoring/combinator.js +92 -0
- package/lib/authoring/compete.js +76 -0
- package/lib/authoring/compose-hotspot.js +314 -0
- package/lib/authoring/compress.js +279 -0
- package/lib/authoring/concept-net.js +254 -0
- package/lib/authoring/concepts.js +66 -0
- package/lib/authoring/context-project.js +277 -0
- package/lib/authoring/contract.js +476 -0
- package/lib/authoring/corpus-pack.js +125 -0
- package/lib/authoring/cost-probe.js +86 -0
- package/lib/authoring/crystallize.js +555 -0
- package/lib/authoring/dag-decompose.js +256 -0
- package/lib/authoring/dataset-adapter.js +253 -0
- package/lib/authoring/debug-provider.js +92 -0
- package/lib/authoring/decompose.js +343 -0
- package/lib/authoring/emittability.js +236 -0
- package/lib/authoring/equilibrium.js +160 -0
- package/lib/authoring/extract.js +153 -0
- package/lib/authoring/forest.js +55 -0
- package/lib/authoring/forge-fallback.js +116 -0
- package/lib/authoring/givens.js +121 -0
- package/lib/authoring/glossary.js +177 -0
- package/lib/authoring/grammar-graph.js +191 -0
- package/lib/authoring/granularity.js +80 -0
- package/lib/authoring/graph-net.js +108 -0
- package/lib/authoring/ground.js +148 -0
- package/lib/authoring/higher-order.js +85 -0
- package/lib/authoring/hotspot.js +132 -0
- package/lib/authoring/hysteresis.js +85 -0
- package/lib/authoring/index.js +106 -0
- package/lib/authoring/lattice-morphism.js +61 -0
- package/lib/authoring/lattice-pack.js +148 -0
- package/lib/authoring/leaf-io.js +77 -0
- package/lib/authoring/library.js +156 -0
- package/lib/authoring/lifecycle.js +92 -0
- package/lib/authoring/loop.js +193 -0
- package/lib/authoring/master-loop.js +132 -0
- package/lib/authoring/mdl.js +209 -0
- package/lib/authoring/memo-stability.js +122 -0
- package/lib/authoring/method-explorer.js +138 -0
- package/lib/authoring/method-pack.js +181 -0
- package/lib/authoring/method.js +202 -0
- package/lib/authoring/mine.js +510 -0
- package/lib/authoring/mount.js +119 -0
- package/lib/authoring/negotiate.js +100 -0
- package/lib/authoring/parametric.js +139 -0
- package/lib/authoring/reaggregate.js +75 -0
- package/lib/authoring/rebalance.js +157 -0
- package/lib/authoring/recall.js +135 -0
- package/lib/authoring/registry.js +350 -0
- package/lib/authoring/relearn.js +147 -0
- package/lib/authoring/retention.js +89 -0
- package/lib/authoring/segment-proxy.js +114 -0
- package/lib/authoring/serve-leaf.js +64 -0
- package/lib/authoring/slot-aware-serve.js +77 -0
- package/lib/authoring/sound-invoke.js +59 -0
- package/lib/authoring/split-serve.js +74 -0
- package/lib/authoring/ste.js +239 -0
- package/lib/authoring/stock.js +101 -0
- package/lib/authoring/store.js +94 -0
- package/lib/authoring/supervise.js +138 -0
- package/lib/authoring/support.js +131 -0
- package/lib/authoring/task-mirror.js +51 -0
- package/lib/authoring/typed-loop.js +160 -0
- package/lib/authoring/validate.js +569 -0
- package/lib/authoring/widen.js +88 -0
- package/lib/combos/appliance.js +189 -0
- package/lib/combos/critique.js +377 -0
- package/lib/combos/defaults.js +86 -0
- package/lib/combos/durable-runner.js +115 -0
- package/lib/combos/forge.js +209 -0
- package/lib/combos/index.js +86 -0
- package/lib/combos/learning-library.js +253 -0
- package/lib/combos/mixture-serve.js +193 -0
- package/lib/combos/plan-loop.js +192 -0
- package/lib/combos/proxy-cache.js +272 -0
- package/lib/combos/self-mod.js +94 -0
- package/lib/durable/audit.js +102 -0
- package/lib/durable/checkpoint-store.js +550 -0
- package/lib/durable/fold.js +62 -0
- package/lib/durable/interpreter.js +242 -0
- package/lib/durable/xlate.js +185 -0
- package/lib/graph/Graph.js +2486 -0
- package/lib/graph/expr.js +307 -0
- package/lib/graph/index.js +26 -0
- package/lib/graph/log.js +134 -0
- package/lib/graph/objects/Concept.js +382 -0
- package/lib/graph/objects/Entity.js +503 -0
- package/lib/graph/objects/Node.js +57 -0
- package/lib/graph/objects/PathMap.js +557 -0
- package/lib/graph/objects/Segment.js +100 -0
- package/lib/graph/tasks/stabilize.js +59 -0
- package/lib/graph/tasks/taskflow.js +247 -0
- package/lib/index.js +122 -0
- package/lib/load.js +132 -0
- package/lib/providers/backends.js +117 -0
- package/lib/providers/borderline.js +129 -0
- package/lib/providers/cache.js +226 -0
- package/lib/providers/canonicalize.js +198 -0
- package/lib/providers/constat.js +107 -0
- package/lib/providers/geo.js +53 -0
- package/lib/providers/index.js +116 -0
- package/lib/providers/intake.js +263 -0
- package/lib/providers/llm-local.js +88 -0
- package/lib/providers/llm.js +236 -0
- package/lib/providers/local-host.js +197 -0
- package/lib/providers/merge-consistency.js +162 -0
- package/lib/providers/nogood.js +149 -0
- package/lib/providers/reason-loop.js +223 -0
- package/lib/providers/semiring.js +285 -0
- package/lib/providers/solver-fork.js +163 -0
- package/lib/providers/stats.js +228 -0
- package/lib/providers/verify.js +120 -0
- package/lib/runtime/index.js +135 -0
- package/lib/runtime/invoke-pool.js +92 -0
- package/lib/runtime/protocol.js +210 -0
- package/lib/runtime/transport-socket.js +77 -0
- package/lib/runtime/worker-entry.js +31 -0
- package/lib/sg/cli.js +601 -0
- package/lib/sg/log-sinks.js +214 -0
- package/lib/sg/mcp.js +395 -0
- package/lib/sg/proxy-run.js +66 -0
- package/lib/sg/serve-demo.js +185 -0
- package/lib/sg/serve.js +205 -0
- package/lib/sg/trace.js +118 -0
- package/lib/studio/protocol.js +45 -0
- package/lib/studio/public/app.js +310 -0
- package/lib/studio/public/components/ConceptEditor.js +43 -0
- package/lib/studio/public/components/ConceptTree.js +43 -0
- package/lib/studio/public/components/CorpusPanel.js +68 -0
- package/lib/studio/public/components/ForkTree.js +36 -0
- package/lib/studio/public/components/GrammarGraph.js +99 -0
- package/lib/studio/public/components/GraphCanvas.js +156 -0
- package/lib/studio/public/components/Inspector.js +48 -0
- package/lib/studio/public/components/LearningPanel.js +85 -0
- package/lib/studio/public/components/PromptConsole.js +46 -0
- package/lib/studio/public/components/ProviderTrace.js +39 -0
- package/lib/studio/public/components/SessionSplit.js +47 -0
- package/lib/studio/public/components/TilingOverlay.js +40 -0
- package/lib/studio/public/components/Timeline.js +57 -0
- package/lib/studio/public/index.html +26 -0
- package/lib/studio/public/styles.css +185 -0
- package/lib/studio/public/ws.js +58 -0
- package/lib/studio/server.js +130 -0
- package/lib/studio/session.js +397 -0
- package/lib/studio/studio.js +149 -0
- package/package.json +42 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Nathanael Braun
|
|
3
|
+
* @author : Nathanael BRAUN <pp9ping@gmail.com>
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
'use strict';
|
|
19
|
+
/**
|
|
20
|
+
* CLI log sinks — live in lib/sg/ because they touch fs/TTY and must stay out of
|
|
21
|
+
* the fs-free engine core. A sink is fn(record); attach with graph.logger.addSink.
|
|
22
|
+
*
|
|
23
|
+
* plain : one clean line per record, no cursor control (pipe/CI friendly)
|
|
24
|
+
* dashboard : a fixed top stats pane + a scrolling log region (TTY only, zero-dep
|
|
25
|
+
* ANSI via a DECSTBM scroll region). Degrades to plain off a TTY.
|
|
26
|
+
* file : append to a journal (.jsonl -> JSON lines, else formatted text)
|
|
27
|
+
*
|
|
28
|
+
* Per-sink level: the graph logger threshold is set to the MOST permissive level any
|
|
29
|
+
* sink needs (records below it never reach any sink); each sink then drops what is
|
|
30
|
+
* below its own level. This lets the file journal be verbose while the console is info.
|
|
31
|
+
*/
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const util = require('util');
|
|
34
|
+
const { LEVELS } = require('../graph/log.js');
|
|
35
|
+
|
|
36
|
+
const PALETTE = { error: 31, warn: 33, log: 37, info: 36, verbose: 90 }; // ANSI fg codes
|
|
37
|
+
|
|
38
|
+
function passes ( level, threshold ) { return LEVELS[level] <= LEVELS[threshold]; }
|
|
39
|
+
function mostPermissive ( names ) { return names.reduce(function ( a, b ) { return LEVELS[b] > LEVELS[a] ? b : a; }); }
|
|
40
|
+
|
|
41
|
+
function two ( n ) { return (n < 10 ? '0' : '') + n; }
|
|
42
|
+
function hhmmss ( ts ) { var d = new Date(ts); return two(d.getHours()) + ':' + two(d.getMinutes()) + ':' + two(d.getSeconds()); }
|
|
43
|
+
|
|
44
|
+
function ctxStr ( ctx ) {
|
|
45
|
+
if ( !ctx ) return '';
|
|
46
|
+
var bits = [];
|
|
47
|
+
if ( ctx.concept ) bits.push(ctx.concept);
|
|
48
|
+
if ( ctx.target ) bits.push((ctx.type ? ctx.type[0] + ':' : '') + ctx.target);
|
|
49
|
+
return bits.length ? '·' + bits.join('/') : '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Render msg + args the way console does (printf %s/%d/%j, leftover args appended),
|
|
53
|
+
// so the engine's existing printf-style messages format correctly. Sanitized Errors
|
|
54
|
+
// ({message,stack}) are shown as their stack string rather than an inline object.
|
|
55
|
+
function formatBody ( rec ) {
|
|
56
|
+
var args = (rec.args || []).map(function ( a ) { return (a && a.stack) ? a.stack : a; });
|
|
57
|
+
return util.format.apply(util, [rec.msg].concat(args));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatLine ( rec, color ) {
|
|
61
|
+
var head = hhmmss(rec.ts) + ' ' + rec.level.toUpperCase().padEnd(7) + ' [' + rec.label + ctxStr(rec.ctx) + '] ';
|
|
62
|
+
var line = head + formatBody(rec);
|
|
63
|
+
if ( color && PALETTE[rec.level] ) return '\x1b[' + PALETTE[rec.level] + 'm' + line + '\x1b[0m';
|
|
64
|
+
return line;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createPlainSink ( opts ) {
|
|
68
|
+
opts = opts || {};
|
|
69
|
+
var stream = opts.stream || process.stdout;
|
|
70
|
+
var color = opts.color != null ? opts.color : !!stream.isTTY;
|
|
71
|
+
var min = opts.level || 'verbose';
|
|
72
|
+
return function ( rec ) { if ( passes(rec.level, min) ) stream.write(formatLine(rec, color) + '\n'); };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createFileSink ( opts ) {
|
|
76
|
+
var fd = fs.openSync(opts.path, 'a');
|
|
77
|
+
var jsonl = /\.jsonl$/i.test(opts.path);
|
|
78
|
+
var min = opts.level || 'verbose';
|
|
79
|
+
return function ( rec ) {
|
|
80
|
+
if ( !passes(rec.level, min) ) return;
|
|
81
|
+
fs.writeSync(fd, (jsonl ? JSON.stringify(rec) : formatLine(rec, false)) + '\n');
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Live snapshot of the graph's engine state for the status bar (pure / read-only).
|
|
86
|
+
function graphStats ( g ) {
|
|
87
|
+
g = g || {};
|
|
88
|
+
var unstable = g._unstable || [], nodes = 0, segs = 0, other = 0;
|
|
89
|
+
for ( var i = 0 ; i < unstable.length ; i++ ) {
|
|
90
|
+
var u = unstable[i], e = u && (u._etty ? u._etty._ : u._);
|
|
91
|
+
if ( e && e.Node ) nodes++; else if ( e && e.Segment ) segs++; else other++;
|
|
92
|
+
}
|
|
93
|
+
// the main loop is _loopTF driven by _taskFlow; its pending work = queued tasks
|
|
94
|
+
var tf = g._taskFlow || {};
|
|
95
|
+
var queue = (tf.doAfter ? tf.doAfter.length : 0) + (tf.todo ? Math.max(0, tf.todo.length - (tf._pos || 0)) : 0);
|
|
96
|
+
var stats = g._statsByProvider || {}, provMs = 0;
|
|
97
|
+
for ( var k in stats ) provMs += stats[k];
|
|
98
|
+
return {
|
|
99
|
+
state : g._stabilizing ? 'stabilizing' : (unstable.length ? 'pending' : 'stable'),
|
|
100
|
+
unstable: unstable.length, nodes: nodes, segments: segs, other: other,
|
|
101
|
+
pending : (g._pending || []).length, stable: (g._stable || []).length,
|
|
102
|
+
queue : queue, locks: tf.locks || 0,
|
|
103
|
+
rev : g._rev || 0, applies: g._applyId || 0, provMs: provMs
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// One-line status text (no positioning ANSI), padded/truncated to `width`.
|
|
108
|
+
function formatStatusBar ( g, width, elapsedS ) {
|
|
109
|
+
var s = graphStats(g);
|
|
110
|
+
var dot = s.state === 'stable' ? '●' : '◐';
|
|
111
|
+
var parts = [
|
|
112
|
+
dot + ' ' + s.state,
|
|
113
|
+
'obj ' + (s.stable + s.unstable + s.pending),
|
|
114
|
+
'unstable ' + s.unstable + ' (N' + s.nodes + '/S' + s.segments + (s.other ? '/?' + s.other : '') + ')',
|
|
115
|
+
'queue ' + s.queue,
|
|
116
|
+
'rev ' + s.rev,
|
|
117
|
+
'applies ' + s.applies
|
|
118
|
+
];
|
|
119
|
+
if ( s.provMs ) parts.push('prov ' + (Math.round(s.provMs / 100) / 10) + 's');
|
|
120
|
+
if ( elapsedS != null ) parts.push(elapsedS + 's');
|
|
121
|
+
var line = ' ' + parts.join(' │ ') + ' ';
|
|
122
|
+
width = width || 80;
|
|
123
|
+
return line.length > width ? line.slice(0, width) : line + ' '.repeat(width - line.length);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Styled boot banner: "SKYNET·GRAPH vX" + tagline (cyan/bold; plain if !color).
|
|
127
|
+
function banner ( version, color ) {
|
|
128
|
+
var lines = [
|
|
129
|
+
' ⛓ SKYNET · GRAPH v' + (version || '?'),
|
|
130
|
+
' rule-driven knowledge-graph reasoning substrate'
|
|
131
|
+
];
|
|
132
|
+
var w = Math.max(lines[0].length, lines[1].length) + 2;
|
|
133
|
+
var bar = '─'.repeat(w);
|
|
134
|
+
if ( !color ) return ['┌' + bar + '┐', '│' + lines[0] + ' '.repeat(w - lines[0].length) + '│',
|
|
135
|
+
'│' + lines[1] + ' '.repeat(w - lines[1].length) + '│', '└' + bar + '┘'].join('\n');
|
|
136
|
+
var C = '\x1b[1;36m', R = '\x1b[0m', D = '\x1b[2m';
|
|
137
|
+
return [
|
|
138
|
+
C + '┌' + bar + '┐' + R,
|
|
139
|
+
C + '│' + R + lines[0] + ' '.repeat(w - lines[0].length) + C + '│' + R,
|
|
140
|
+
C + '│' + R + D + lines[1] + R + ' '.repeat(w - lines[1].length) + C + '│' + R,
|
|
141
|
+
C + '└' + bar + '┘' + R
|
|
142
|
+
].join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* TTY dashboard: a live STATUS BAR kept at the bottom of the output over normal
|
|
147
|
+
* COLORED scrolling logs (graph state, unstable nodes/segments, main-loop queue,
|
|
148
|
+
* rev, applies, provider time, elapsed).
|
|
149
|
+
*
|
|
150
|
+
* Robust "reprint-in-place" technique (like npm/cargo progress bars) — NO scroll
|
|
151
|
+
* region, NO absolute cursor positioning, NO save/restore (all of which are flaky
|
|
152
|
+
* across WSL/tmux/embedded terminals and were flooding the scrollback): the bar is
|
|
153
|
+
* always the LAST thing written and never gets a trailing newline, so the cursor
|
|
154
|
+
* stays parked on it. To log: erase the bar line (`\r ESC[2K`), write the log line +
|
|
155
|
+
* `\n` (the terminal scrolls naturally), then reprint the bar on the new last line.
|
|
156
|
+
* A timer refreshes the bar in place. Off a TTY it degrades to a plain sink.
|
|
157
|
+
*/
|
|
158
|
+
function createDashboardSink ( opts ) {
|
|
159
|
+
// either a static `graph` or a `getGraph()` (studio has many sessions → the active one)
|
|
160
|
+
var getGraph = opts.getGraph || function () { return opts.graph; };
|
|
161
|
+
var stream = opts.stream || process.stdout;
|
|
162
|
+
var min = opts.level || 'verbose';
|
|
163
|
+
if ( !stream.isTTY ) return createPlainSink({ stream: stream, level: min }); // degrade
|
|
164
|
+
|
|
165
|
+
var start = Date.now(), timer = null, active = false;
|
|
166
|
+
function cols () { return stream.columns || 80; }
|
|
167
|
+
|
|
168
|
+
function barText () {
|
|
169
|
+
var w = cols(), g = getGraph();
|
|
170
|
+
var elapsed = Math.round((Date.now() - start) / 100) / 10;
|
|
171
|
+
var line;
|
|
172
|
+
if ( g ) line = formatStatusBar(g, w, elapsed);
|
|
173
|
+
else { line = ' ◌ idle — no graph loaded ' + elapsed + 's'; line = line.length > w ? line.slice(0, w) : line + ' '.repeat(w - line.length); }
|
|
174
|
+
return '\x1b[7m' + line + '\x1b[0m';
|
|
175
|
+
}
|
|
176
|
+
function clearBarLine () { stream.write('\r\x1b[2K'); } // col 0 + erase the whole line
|
|
177
|
+
function drawBar () { clearBarLine(); stream.write(barText() + '\r'); } // bar, cursor parked at its start (no newline)
|
|
178
|
+
|
|
179
|
+
function mount () { active = true; drawBar(); timer = setInterval(drawBar, 250); if ( timer.unref ) timer.unref(); }
|
|
180
|
+
|
|
181
|
+
function sink ( rec ) {
|
|
182
|
+
if ( !active ) mount();
|
|
183
|
+
if ( !passes(rec.level, min) ) return;
|
|
184
|
+
clearBarLine(); // wipe the bar off the last line
|
|
185
|
+
stream.write(formatLine(rec, true) + '\n'); // print the log; the terminal scrolls
|
|
186
|
+
drawBar(); // reprint the bar on the new last line
|
|
187
|
+
}
|
|
188
|
+
sink.close = function () {
|
|
189
|
+
if ( timer ) { clearInterval(timer); timer = null; }
|
|
190
|
+
if ( active ) { clearBarLine(); active = false; } // remove the bar, leave a clean line
|
|
191
|
+
};
|
|
192
|
+
return sink;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Log-only mode has no fixed bar, so stats are emitted into the log stream instead:
|
|
196
|
+
// a periodic info line with the same fields. (In dashboard mode the bar owns the stats
|
|
197
|
+
// and this is NOT used — stats never appear in the logs there.) Returns the timer.
|
|
198
|
+
function startStatsLogger ( logger, getGraph, intervalMs ) {
|
|
199
|
+
var t = setInterval(function () {
|
|
200
|
+
var g = getGraph && getGraph();
|
|
201
|
+
if ( !g ) return;
|
|
202
|
+
var s = graphStats(g);
|
|
203
|
+
logger.info('stats — %s | unstable %s (N%s/S%s) | queue %s | rev %s | applies %s',
|
|
204
|
+
s.state, s.unstable, s.nodes, s.segments, s.queue, s.rev, s.applies);
|
|
205
|
+
}, intervalMs || 2000);
|
|
206
|
+
if ( t.unref ) t.unref();
|
|
207
|
+
return t;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
createPlainSink, createFileSink, createDashboardSink, startStatsLogger,
|
|
212
|
+
formatLine, formatBody, ctxStr, hhmmss, mostPermissive, passes,
|
|
213
|
+
graphStats, formatStatusBar, banner
|
|
214
|
+
};
|
package/lib/sg/mcp.js
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Nathanael Braun
|
|
3
|
+
* @author : Nathanael BRAUN <pp9ping@gmail.com>
|
|
4
|
+
*
|
|
5
|
+
* AGPL-3.0-or-later. See <https://www.gnu.org/licenses/>.
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
/**
|
|
9
|
+
* `sg mcp` — the MCP (Model Context Protocol) TOOLS server (roadmap FINIR, F2): the agentic surface where the
|
|
10
|
+
* TYPED capabilities become tools an agent host (Claude Code, any MCP client) consumes. Where `sg serve` sells
|
|
11
|
+
* the economy behind a generic chat wire, THIS surface exposes the differentiators STRUCTURED:
|
|
12
|
+
* • `ask` — answer OR the TYPED REFUSAL as data ({status:'refused', reason, missing:[…]} — a refusal is a
|
|
13
|
+
* typed ANSWER that names the missing requirement, never an error string);
|
|
14
|
+
* • `drift` / `metrics` — anti-drift invalidation + the economy readout (proxy mode);
|
|
15
|
+
* • `lattice_load` — LEARNING THROUGH THE GATE: a `.sgc kind:'lattice'` bundle grows the held registry via
|
|
16
|
+
* `loadLattice` (version-gated, conflicting rings REJECTED by the confluence-checked merge). There is NO
|
|
17
|
+
* tool that writes the stock or the registry directly — enrichment is ask-escalation or this gated load;
|
|
18
|
+
* • `methods_describe` / `lattice_rings` — the explorer bricks à nu (pure file reads, no model);
|
|
19
|
+
* • `trace_tail` — the debug contract: the shared logger's ring buffer, filterable {level, concept, applyId}.
|
|
20
|
+
*
|
|
21
|
+
* THIN assembly (doctrine): `createMcpServer` is a PURE JSON-RPC dispatcher (stub-testable, no pipe),
|
|
22
|
+
* `defaultTools` wires tools onto the existing combos/bricks, `startMcpStdio` is the zero-dep stdio transport
|
|
23
|
+
* (one JSON-RPC message per line — the MCP stdio framing). GPU-bound model resolution stays in cli.js.
|
|
24
|
+
* stdout is the PROTOCOL channel: a host process must log to stderr only.
|
|
25
|
+
*
|
|
26
|
+
* Register with an MCP host, e.g.: claude mcp add sg -- node bin/sg mcp --frontier-model <gguf> --store ./stock.json
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
var fs = require('fs');
|
|
30
|
+
|
|
31
|
+
var PROTOCOL_VERSION = '2024-11-05';
|
|
32
|
+
|
|
33
|
+
/** Read + parse a JSON artifact (a `.sgc` bundle or a bare JSON file). Throws a plain Error on failure. */
|
|
34
|
+
function readJson( file ) {
|
|
35
|
+
return JSON.parse(fs.readFileSync(String(file), 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The default tool set over the existing bricks. Every tool is { name, description, inputSchema, call(args) };
|
|
40
|
+
* `call` returns the STRUCTURED result (the server wraps it in MCP content). Wire what the host resolved:
|
|
41
|
+
* @param w.proxy a createProxyCache instance (proxy mode: ask/drift/metrics/lattice_load).
|
|
42
|
+
* @param w.appliance a createAppliance instance (typed-QA mode: ask returns answer OR the typed refusal).
|
|
43
|
+
* @param w.logger the shared logger (trace_tail). Optional.
|
|
44
|
+
*/
|
|
45
|
+
function defaultTools( w ) {
|
|
46
|
+
w = w || {};
|
|
47
|
+
var tools = [];
|
|
48
|
+
var q = { type: 'object', properties: { query: { type: 'string', description: 'the question' } }, required: ['query'] };
|
|
49
|
+
|
|
50
|
+
if ( w.appliance || w.proxy ) tools.push({
|
|
51
|
+
name: 'ask',
|
|
52
|
+
description: w.appliance
|
|
53
|
+
? 'Typed QA: answer from the declared domain, or a TYPED REFUSAL naming the missing requirement ({status, answer?, reason?, missing?[]}). A refusal is data, not an error.'
|
|
54
|
+
: 'Local-first proxy ask: a covered query is served from the verified local stock (0 frontier calls), a miss escalates to the frontier and enriches the stock ({answer, source, cached, cost}).',
|
|
55
|
+
inputSchema: q,
|
|
56
|
+
call: async function ( a ) {
|
|
57
|
+
if ( w.appliance ) return await w.appliance.answer(String(a.query));
|
|
58
|
+
var r = await w.proxy.answer(String(a.query));
|
|
59
|
+
return { answer: r.answer, source: r.source, cached: !!r.cached, cost: r.cost };
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if ( w.proxy ) {
|
|
64
|
+
tools.push({
|
|
65
|
+
name: 'drift',
|
|
66
|
+
description: 'Anti-drift: invalidate the stock entry covering this query — the next ask re-escalates to the frontier.',
|
|
67
|
+
inputSchema: q,
|
|
68
|
+
call: async function ( a ) { await w.proxy.drift(String(a.query)); return { drifted: true, query: String(a.query) }; }
|
|
69
|
+
});
|
|
70
|
+
tools.push({
|
|
71
|
+
name: 'metrics',
|
|
72
|
+
description: 'The proxy economy readout: served/local/frontier counts, coverage, stock size/reuse.',
|
|
73
|
+
inputSchema: { type: 'object', properties: {} },
|
|
74
|
+
call: async function () { return w.proxy.metrics(); }
|
|
75
|
+
});
|
|
76
|
+
if ( w.proxy.library && typeof w.proxy.library.loadLattice === 'function' ) tools.push({
|
|
77
|
+
name: 'lattice_load',
|
|
78
|
+
description: 'LEARN THROUGH THE GATE: load a `.sgc kind:\'lattice\'` bundle — the held registry grows via the version-gated admission merge (a conflicting ring is REJECTED, never merged). The only registry write path.',
|
|
79
|
+
inputSchema: { type: 'object', properties: { file: { type: 'string', description: 'path to a .sgc lattice bundle' } }, required: ['file'] },
|
|
80
|
+
call: async function ( a ) { return w.proxy.library.loadLattice(readJson(a.file)); }
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// C9 — the external CRITICAL MIND as a tool (design A5, rhetoric lanes). The host LLM supplies the
|
|
85
|
+
// topic (+ optional statements/viewpoints); the lib runs ITS local model through the witness-gated
|
|
86
|
+
// loop and returns the typed LEDGER + a certification-aware verdict. The frame status
|
|
87
|
+
// (FREE/MATERIAL/DECLARED) is ALWAYS in the payload; below the measured margin bound the verdict is
|
|
88
|
+
// an honest UNDECIDED — the ledger + counts are the deliverable, never a fake weighing.
|
|
89
|
+
if ( typeof w.critiqueAsk === 'function' ) tools.push({
|
|
90
|
+
name: 'critique',
|
|
91
|
+
description: 'Run the external critical mind on a question: declared viewpoints established through a witness gate over a statement pool, anchored generation of missing theses (0-fabrication), typed ledger, per-side synthesis, and a certification-aware verdict (mechanical only at the measured margin bound; otherwise UNDECIDED with counts). Supply `statements` ("PRO: ..."/"CON: ..." lines) to critique YOUR material (frame MATERIAL) and/or `viewpoints` to declare the frame (DECLARED); with neither, the pool is model-brainstormed and the payload SAYS so (frame FREE).',
|
|
92
|
+
inputSchema: { type: 'object', properties: {
|
|
93
|
+
topic: { type: 'string', description: 'the question under critique' },
|
|
94
|
+
statements: { type: 'array', items: { type: 'string' }, description: 'optional pool, one statement per entry, each prefixed "PRO: " or "CON: "' },
|
|
95
|
+
viewpoints: { type: 'array', items: { type: 'string' }, description: 'optional declared viewpoints (the decision frame becomes DECLARED)' },
|
|
96
|
+
polish: { type: 'boolean', description: 'add a presentation-only rewrite of the prose (content-locked)' },
|
|
97
|
+
}, required: ['topic'] },
|
|
98
|
+
call: async function ( a ) {
|
|
99
|
+
if ( !w._criticalMind ) w._criticalMind = require('../combos/critique.js').createCriticalMind({ ask: w.critiqueAsk });
|
|
100
|
+
var r = await w._criticalMind.run({ topic: String(a.topic), statements: a.statements, viewpoints: a.viewpoints, polish: !!a.polish });
|
|
101
|
+
// The ITERATION CONTRACT (the server cannot reach the web/host tools — YOU can): OPEN points
|
|
102
|
+
// and a sub-threshold margin are a TYPED REQUEST FOR DATA. Gather real statements that bear
|
|
103
|
+
// on them (web, docs, your context), then call `critique` again with `statements` — the frame
|
|
104
|
+
// upgrades to MATERIAL and the loop converges at the host level (bounded rounds are yours).
|
|
105
|
+
var open = (r.ledger || []).filter(function ( e ) { return e.status === 'open'; });
|
|
106
|
+
var advice = null;
|
|
107
|
+
if ( r.error ) advice = 'pool too small: supply `statements` ("PRO: ..."/"CON: ..." entries).';
|
|
108
|
+
else if ( open.length || r.verdict === 'UNDECIDED' ) advice = 'This is a typed data request, not a dead end: '
|
|
109
|
+
+ (open.length ? 'gather statements that genuinely bear on the OPEN points [' + open.map(function ( e ) { return e.text.slice(0, 50); }).join(' · ') + '] ' : '')
|
|
110
|
+
+ (r.verdict === 'UNDECIDED' ? 'and/or on whichever side you believe is under-evidenced ' : '')
|
|
111
|
+
+ 'from sources YOU can reach (web/docs), then call critique again with statements=[...] — the frame becomes MATERIAL and the margin can move honestly.';
|
|
112
|
+
return { frameStatus: r.frameStatus, verdict: r.verdict, basis: r.basis, norm: r.norm, counts: r.counts, margin: r.margin, threshold: r.threshold,
|
|
113
|
+
ledger: r.ledger, synthesis: r.synthesis, prose: r.polished || r.prose, advice: advice, error: r.error };
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── the ASSISTANT lanes (design 2026-07-10, owner-approved): the lib as the LLM's assistant. TWO EXPLICIT
|
|
118
|
+
// LANES — SOFT (advisory, no guarantee, says so) and HARD (gated, typed verdict, NON-BYPASSABLE: forcing
|
|
119
|
+
// downgrades PROVENANCE, never the admission) — plus the INSTANCES lane (the P3 shared-instance pool).
|
|
120
|
+
|
|
121
|
+
// SOFT: `hint` — the orientation menu (certified shapes, slot-glossed). Score lever, no guarantee attached.
|
|
122
|
+
if ( w.hints && Array.isArray(w.hints.certifiedShapes) ) tools.push({
|
|
123
|
+
name: 'hint',
|
|
124
|
+
description: 'SOFT lane (advisory, no guarantee): the certified-shape menu for this query, slot-glossed — orientation lifts the score (+13 to +36 pts measured) but admission is a separate, gated step (`propose`).',
|
|
125
|
+
inputSchema: q,
|
|
126
|
+
call: async function ( a ) {
|
|
127
|
+
var qualifyMenu = require('../combos/mixture-serve.js').qualifyMenu;
|
|
128
|
+
var shapes = (typeof w.hints.proposeMenu === 'function' && w.hints.proposeMenu(String(a.query))) || w.hints.certifiedShapes;
|
|
129
|
+
return { menu: w.hints.gloss ? qualifyMenu(shapes, w.hints.gloss) : shapes.slice(), advisory: true, certified: true };
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// HARD: `propose` — the negotiate step exposed (the LLM host IS the proposer; bounded rounds are its discipline).
|
|
134
|
+
if ( w.gate && typeof w.gate.check === 'function' ) tools.push({
|
|
135
|
+
name: 'propose',
|
|
136
|
+
description: 'HARD lane (the gate NEVER yields): submit a typed proposal — admitted, or refused with blame + the admissible options ENUMERATED THROUGH the gate (tested, never guessed). force=true does NOT admit: it records the result as untrusted provenance (traced, auditable, outside the certified layer).',
|
|
137
|
+
inputSchema: { type: 'object', properties: {
|
|
138
|
+
proposal: { type: 'object', description: 'the typed proposal (domain-shaped)' },
|
|
139
|
+
force: { type: 'boolean', description: 'insist despite a refusal — recorded untrusted, never admitted' }
|
|
140
|
+
}, required: ['proposal'] },
|
|
141
|
+
call: async function ( a ) {
|
|
142
|
+
var v = await w.gate.check(a.proposal);
|
|
143
|
+
if ( v && v.ok ) return { status: 'admitted', certified: true };
|
|
144
|
+
var blame = (v && v.blame) || 'refused';
|
|
145
|
+
if ( a.force ) {
|
|
146
|
+
if ( typeof w.gate.record === 'function' ) w.gate.record(a.proposal, { forced: true, blame: blame });
|
|
147
|
+
return { status: 'recorded-untrusted', certified: false, blame: blame };
|
|
148
|
+
}
|
|
149
|
+
var options = [];
|
|
150
|
+
if ( typeof w.gate.optionsOf === 'function' ) {
|
|
151
|
+
var cands = (await w.gate.optionsOf(a.proposal)) || [];
|
|
152
|
+
for ( var i = 0; i < cands.length && options.length < 8; i++ ) {
|
|
153
|
+
var ov = await w.gate.check(cands[i]);
|
|
154
|
+
if ( ov && ov.ok ) options.push(cands[i]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { status: 'refused', blame: blame, options: options };
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// SOFT: `state_recall` + gated `state_note` — the CERTIFIED TASK-STATE (synthesized by stabilization/JTMS,
|
|
162
|
+
// bounded projection — not an append log). `note` posts a TYPED fact through the host's SEQUENCED ingest.
|
|
163
|
+
if ( w.state && typeof w.state.recall === 'function' ) tools.push({
|
|
164
|
+
name: 'state_recall',
|
|
165
|
+
description: 'SOFT lane (advisory read): the synthesized task-state — typed facts with provenance + the open frontier, bounded projection (Σ_sep). A drifted premise RETRACTS its facts (JTMS), it does not rot.',
|
|
166
|
+
inputSchema: { type: 'object', properties: {} },
|
|
167
|
+
call: async function () { return w.state.recall(); }
|
|
168
|
+
});
|
|
169
|
+
if ( w.state && typeof w.state.note === 'function' ) tools.push({
|
|
170
|
+
name: 'state_note',
|
|
171
|
+
description: 'Post ONE typed fact ({key, value}) to the task-state through the sequenced ingest (never an out-of-band write); the graph synthesizes — the note lands with provenance and is retractable.',
|
|
172
|
+
inputSchema: { type: 'object', properties: { key: { type: 'string' }, value: {} }, required: ['key'] },
|
|
173
|
+
call: async function ( a ) { return await w.state.note({ key: String(a.key), value: a.value }); }
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// INSTANCES: the P3 shared-instance pool — invoke a concept-method by contract key; list/release instances.
|
|
177
|
+
if ( w.pool && typeof w.pool.invoke === 'function' ) {
|
|
178
|
+
tools.push({
|
|
179
|
+
name: 'graph_invoke',
|
|
180
|
+
description: 'Invoke a concept-method on its SHARED instance (P3 pool, keyed by contract): stabilizes the seed on a fresh graph in the worker and returns ONLY the bounded projection (summaryFacts + write-footprint) — a refusal (frontier drift / post-violation) is a typed answer.',
|
|
181
|
+
inputSchema: { type: 'object', properties: {
|
|
182
|
+
libraryKey: { type: 'string', description: 'the contract/dispatch key' },
|
|
183
|
+
seed: { type: 'object', description: 'the case seed (slot bindings included)' }
|
|
184
|
+
}, required: ['libraryKey'] },
|
|
185
|
+
call: async function ( a ) { return await w.pool.invoke(String(a.libraryKey), { seed: a.seed }); }
|
|
186
|
+
});
|
|
187
|
+
tools.push({
|
|
188
|
+
name: 'graph_instances',
|
|
189
|
+
description: 'The instance lifecycle: list the pooled graph instances (keys, uses) or release one (`release`: its next invoke re-creates it fresh — deactivation, not deletion of anything learned).',
|
|
190
|
+
inputSchema: { type: 'object', properties: { release: { type: 'string', description: 'optional key to release' } } },
|
|
191
|
+
call: async function ( a ) {
|
|
192
|
+
a = a || {};
|
|
193
|
+
if ( a.release ) { var okE = w.pool.evict(String(a.release)); return { released: a.release, ok: okE !== false }; }
|
|
194
|
+
var st = w.pool.stats();
|
|
195
|
+
return { keys: w.pool.keys(), instances: st.instances, uses: st.uses };
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// SOFT: `plan_sync` — the graph plan mirrored onto the HOST's native task list, AT THE HOST'S CHOICE
|
|
201
|
+
// (pull-based: the LLM calls it when it wants a mirror, or never). MCP cannot invoke host tools, so the
|
|
202
|
+
// graph EMITS a typed delta the host applies verbatim (TaskCreate/TaskUpdate, TodoWrite, …).
|
|
203
|
+
if ( w.plan && typeof w.plan.snapshot === 'function' ) ( function () {
|
|
204
|
+
var mirrorState = null;
|
|
205
|
+
tools.push({
|
|
206
|
+
name: 'plan_sync',
|
|
207
|
+
description: 'SOFT lane, OPTIONAL (your choice, pull-based): the typed delta between the graph\'s multistep plan and the last sync — apply the ops VERBATIM to your native task system (create/update/complete/reopen). `complete` means GATE-ADMITTED (not claimed); `reopen` fires when a completed step\'s premise DRIFTED (JTMS retraction). reset=true resends the full state.',
|
|
208
|
+
inputSchema: { type: 'object', properties: { reset: { type: 'boolean', description: 'resend the full plan state' } } },
|
|
209
|
+
call: async function ( a ) {
|
|
210
|
+
if ( a && a.reset ) mirrorState = null;
|
|
211
|
+
var r = require('../authoring/task-mirror.js').diffPlanToTaskOps(await w.plan.snapshot(), mirrorState);
|
|
212
|
+
mirrorState = r.mirror;
|
|
213
|
+
return { taskOps: r.ops };
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
} )();
|
|
217
|
+
|
|
218
|
+
// the explorer bricks à nu — pure file reads, standalone value (no model, no graph).
|
|
219
|
+
tools.push({
|
|
220
|
+
name: 'methods_describe',
|
|
221
|
+
description: 'Describe a concept-method population from a `.sgc kind:\'methods\'` bundle (or a bare library JSON): titles/categories, class distribution, openness (entropy/singletons), coverage vs a declared vocabulary.',
|
|
222
|
+
inputSchema: { type: 'object', properties: { file: { type: 'string' }, registry: { type: 'string', description: 'optional .sgc lattice file for coverage' } }, required: ['file'] },
|
|
223
|
+
call: async function ( a ) {
|
|
224
|
+
var describeLibrary = require('../authoring/method-explorer.js').describeLibrary;
|
|
225
|
+
var art = readJson(a.file);
|
|
226
|
+
var source = (art && art.format === 'sgc' && art.kind === 'methods') ? (art.methods || []) : art;
|
|
227
|
+
var opts = {};
|
|
228
|
+
if ( a.registry ) { var rf = readJson(a.registry); opts.registry = rf && rf.registry ? rf.registry : rf; }
|
|
229
|
+
return describeLibrary(source, opts);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
tools.push({
|
|
233
|
+
name: 'lattice_rings',
|
|
234
|
+
description: 'List the admitted synonym-ring aliases of a `.sgc kind:\'lattice\'` bundle ({key, member, alias} rows), optionally filtered by member or alias substring.',
|
|
235
|
+
inputSchema: { type: 'object', properties: { file: { type: 'string' }, filter: { type: 'string', description: 'optional substring over key/member/alias' } }, required: ['file'] },
|
|
236
|
+
call: async function ( a ) {
|
|
237
|
+
var lp = require('../authoring/lattice-pack.js');
|
|
238
|
+
var art = readJson(a.file);
|
|
239
|
+
var reg;
|
|
240
|
+
try { reg = lp.unpackLattice(art).registry; } // a .sgc lattice bundle…
|
|
241
|
+
catch ( e ) { reg = (art && art.registry) || art; } // …or a bare registry JSON (à-nu)
|
|
242
|
+
var rows = lp.ringsOf(reg);
|
|
243
|
+
if ( a.filter ) {
|
|
244
|
+
var f = String(a.filter).toLowerCase();
|
|
245
|
+
rows = rows.filter(function ( r ) { return (r.key + ' ' + r.member + ' ' + r.alias).toLowerCase().indexOf(f) !== -1; });
|
|
246
|
+
}
|
|
247
|
+
return { rings: rows, count: rows.length };
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if ( w.logger && typeof w.logger.tail === 'function' ) tools.push({
|
|
252
|
+
name: 'trace_tail',
|
|
253
|
+
description: 'Debug: the last N records of the shared log ring buffer, filterable by level/concept/target/applyId — the same records `sg trace` joins by applyId.',
|
|
254
|
+
inputSchema: {
|
|
255
|
+
type: 'object',
|
|
256
|
+
properties: {
|
|
257
|
+
n: { type: 'number', description: 'max records (default 20)' },
|
|
258
|
+
level: { type: 'string' }, concept: { type: 'string' }, target: { type: 'string' }, applyId: { type: 'string' }
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
call: async function ( a ) {
|
|
262
|
+
a = a || {};
|
|
263
|
+
var filter = {};
|
|
264
|
+
['level', 'concept', 'target', 'applyId'].forEach(function ( k ) { if ( a[k] != null ) filter[k] = a[k]; });
|
|
265
|
+
return { records: w.logger.tail(a.n || 20, Object.keys(filter).length ? filter : undefined) };
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return tools;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The PURE MCP server: JSON-RPC 2.0 dispatch over a tool set (no pipe — stub-testable; the transport writes
|
|
274
|
+
* whatever `handle` returns, and a null return means "notification, no response").
|
|
275
|
+
* @param opts.tools [{ name, description, inputSchema, call }] (see defaultTools).
|
|
276
|
+
* @param opts.serverInfo { name, version } advertised on initialize.
|
|
277
|
+
* @returns {{ handle: async (msg) => response|null, tools }}
|
|
278
|
+
*/
|
|
279
|
+
function createMcpServer( opts ) {
|
|
280
|
+
opts = opts || {};
|
|
281
|
+
var tools = opts.tools || [];
|
|
282
|
+
var serverInfo = opts.serverInfo || { name: 'skynet-graph', version: '0.0.0' };
|
|
283
|
+
var byName = {};
|
|
284
|
+
tools.forEach(function ( t ) { byName[t.name] = t; });
|
|
285
|
+
|
|
286
|
+
var reply = function ( id, result ) { return { jsonrpc: '2.0', id: id, result: result }; };
|
|
287
|
+
var rpcError = function ( id, code, message ) { return { jsonrpc: '2.0', id: id, error: { code: code, message: message } }; };
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
tools: tools,
|
|
291
|
+
handle: async function ( msg ) {
|
|
292
|
+
if ( !msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0' ) return rpcError(msg && msg.id != null ? msg.id : null, -32600, 'invalid JSON-RPC 2.0 message');
|
|
293
|
+
var id = msg.id, method = msg.method, params = msg.params || {};
|
|
294
|
+
var notification = (id === undefined);
|
|
295
|
+
|
|
296
|
+
switch ( method ) {
|
|
297
|
+
case 'initialize':
|
|
298
|
+
return reply(id, {
|
|
299
|
+
protocolVersion: params.protocolVersion || PROTOCOL_VERSION,
|
|
300
|
+
capabilities: { tools: {} },
|
|
301
|
+
serverInfo: serverInfo
|
|
302
|
+
});
|
|
303
|
+
case 'notifications/initialized':
|
|
304
|
+
case 'notifications/cancelled':
|
|
305
|
+
return null; // notifications get no response
|
|
306
|
+
case 'ping':
|
|
307
|
+
return reply(id, {});
|
|
308
|
+
case 'tools/list':
|
|
309
|
+
return reply(id, { tools: tools.map(function ( t ) { return { name: t.name, description: t.description, inputSchema: t.inputSchema }; }) });
|
|
310
|
+
case 'tools/call': {
|
|
311
|
+
var tool = byName[params.name];
|
|
312
|
+
if ( !tool ) return rpcError(id, -32602, 'unknown tool: ' + params.name);
|
|
313
|
+
try {
|
|
314
|
+
var result = await tool.call(params.arguments || {});
|
|
315
|
+
// STRUCTURED first (a typed refusal is data): text mirror for hosts without structuredContent.
|
|
316
|
+
return reply(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result, isError: false });
|
|
317
|
+
}
|
|
318
|
+
catch ( e ) {
|
|
319
|
+
// a TOOL failure is a tool-level error (isError), not a protocol error — the host shows it.
|
|
320
|
+
return reply(id, { content: [{ type: 'text', text: String(e && e.message || e) }], isError: true });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
default:
|
|
324
|
+
return notification ? null : rpcError(id, -32601, 'method not found: ' + method);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The zero-dep stdio transport: one JSON-RPC message per line (the MCP stdio framing). Injectable streams for
|
|
332
|
+
* tests; a parse failure answers a JSON-RPC -32700 instead of crashing the server.
|
|
333
|
+
* @param o.server a createMcpServer instance.
|
|
334
|
+
* @param o.input readable (default process.stdin); o.output writable (default process.stdout).
|
|
335
|
+
* @returns {{ close }}
|
|
336
|
+
*/
|
|
337
|
+
function startMcpStdio( o ) {
|
|
338
|
+
o = o || {};
|
|
339
|
+
if ( !o.server || typeof o.server.handle !== 'function' ) throw new Error('startMcpStdio needs o.server (a createMcpServer instance)');
|
|
340
|
+
var input = o.input || process.stdin, output = o.output || process.stdout;
|
|
341
|
+
var buf = '';
|
|
342
|
+
var write = function ( res ) { if ( res ) output.write(JSON.stringify(res) + '\n'); };
|
|
343
|
+
// responses go out in ARRIVAL order (a promise chain) — a sync parse error must not overtake the async
|
|
344
|
+
// handling of an earlier message.
|
|
345
|
+
var chain = Promise.resolve();
|
|
346
|
+
var enqueue = function ( job ) { chain = chain.then(job, job); };
|
|
347
|
+
// the parameter CAPTURES the message per line — several lines can coalesce into one data chunk, and a
|
|
348
|
+
// loop-scoped `var` would leave every queued job handling the LAST parsed message.
|
|
349
|
+
var handleLine = function ( msg ) {
|
|
350
|
+
enqueue(function () {
|
|
351
|
+
return Promise.resolve(o.server.handle(msg)).then(write, function ( e ) {
|
|
352
|
+
write({ jsonrpc: '2.0', id: msg && msg.id != null ? msg.id : null, error: { code: -32603, message: String(e && e.message || e) } });
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
var onData = function ( d ) {
|
|
357
|
+
buf += d;
|
|
358
|
+
var nl;
|
|
359
|
+
while ( (nl = buf.indexOf('\n')) !== -1 ) {
|
|
360
|
+
var line = buf.slice(0, nl).trim();
|
|
361
|
+
buf = buf.slice(nl + 1);
|
|
362
|
+
if ( !line ) continue;
|
|
363
|
+
var msg;
|
|
364
|
+
try { msg = JSON.parse(line); }
|
|
365
|
+
catch ( e ) { enqueue(function () { write({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'parse error' } }); }); continue; }
|
|
366
|
+
handleLine(msg);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
input.setEncoding && input.setEncoding('utf8');
|
|
370
|
+
input.on('data', onData);
|
|
371
|
+
return { close: function () { input.removeListener('data', onData); } };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* stockWiring — a `.sgc kind:'methods'` bundle (or bare {methods}) → the ASSISTANT wirings (hints + gate) over
|
|
376
|
+
* its certified vocabulary (the per-method class key `structure.taskKind`). The cli.js `--stock` bridge: `hint`
|
|
377
|
+
* serves the certified menu, `propose` gates a {shape} proposal against the frozen referential and enumerates
|
|
378
|
+
* the admissible options. Fail-fast on an empty bundle (a silent empty referential would refuse everything).
|
|
379
|
+
*/
|
|
380
|
+
function stockWiring( bundle ) {
|
|
381
|
+
var methods = (bundle && (bundle.methods || (bundle.format === 'sgc' && bundle.kind === 'methods' && bundle.methods))) || null;
|
|
382
|
+
var seen = Object.create(null);
|
|
383
|
+
(methods || []).forEach(function ( m ) { var k = m && m.structure && m.structure.taskKind; if ( k ) seen[k] = true; });
|
|
384
|
+
var shapes = Object.keys(seen).sort();
|
|
385
|
+
if ( !shapes.length ) throw new Error('stockWiring: no certified shapes in the stock bundle (need .sgc kind:methods with structure.taskKind per method)');
|
|
386
|
+
return {
|
|
387
|
+
hints: { certifiedShapes: shapes },
|
|
388
|
+
gate: {
|
|
389
|
+
check: function ( p ) { return shapes.indexOf(p && p.shape) >= 0 ? { ok: true } : { ok: false, blame: 'shape "' + (p && p.shape) + '" ∉ certified referential (' + shapes.length + ' shapes)' }; },
|
|
390
|
+
optionsOf: function () { return shapes.map(function ( shape ) { return { shape: shape }; }); }
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = { createMcpServer: createMcpServer, defaultTools: defaultTools, startMcpStdio: startMcpStdio, stockWiring: stockWiring };
|