@nonbot/mcp 0.2.3 → 0.3.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.
@@ -0,0 +1,280 @@
1
+ export const RELEASE_SUBTYPE = 'release';
2
+ export const RELEASE_KEY = 'releaseId';
3
+ export const DONE_AT_KEY = 'doneAt';
4
+ export const RELEASE_SIZE = { width: 300, height: 184 };
5
+ const dataOf = (n) => (n?.data ?? {});
6
+ export const releaseIdOf = (n) => {
7
+ const v = dataOf(n)[RELEASE_KEY];
8
+ return typeof v === 'string' && v ? v : null;
9
+ };
10
+ export function releasesById(modules) {
11
+ const m = new Map();
12
+ for (const r of modules)
13
+ if (r.subtype === RELEASE_SUBTYPE)
14
+ m.set(r.id, r);
15
+ return m;
16
+ }
17
+ export const releaseIsOpen = (r) => r.data?.open === true;
18
+ export function isHiddenByRelease(n, releases) {
19
+ const rid = releaseIdOf(n);
20
+ if (!rid)
21
+ return false;
22
+ const r = releases.get(rid);
23
+ return !!r && !releaseIsOpen(r);
24
+ }
25
+ export function isFiled(n, releases) {
26
+ const rid = releaseIdOf(n);
27
+ return !!rid && releases.has(rid);
28
+ }
29
+ export function membersOf(releaseId, nodes) {
30
+ return nodes.filter(n => releaseIdOf(n) === releaseId);
31
+ }
32
+ export function subtreeOf(rootIds, nodes, edges = []) {
33
+ const byParent = new Map();
34
+ const byId = new Map();
35
+ for (const n of nodes) {
36
+ byId.set(n.id, n);
37
+ const p = dataOf(n).parentItemId;
38
+ if (typeof p === 'string' && p)
39
+ (byParent.get(p) ?? byParent.set(p, []).get(p)).push(n.id);
40
+ }
41
+ const okr = new Map();
42
+ for (const e of edges)
43
+ if ((e.dependencyType ?? e.type) === 'okr-link')
44
+ (okr.get(e.source) ?? okr.set(e.source, []).get(e.source)).push(e.target);
45
+ const out = new Set();
46
+ const stack = [...rootIds];
47
+ while (stack.length) {
48
+ const id = stack.pop();
49
+ if (out.has(id) || !byId.has(id))
50
+ continue;
51
+ out.add(id);
52
+ for (const c of byParent.get(id) ?? [])
53
+ stack.push(c);
54
+ const linked = dataOf(byId.get(id)).linkedEpicIds;
55
+ if (Array.isArray(linked))
56
+ for (const x of linked)
57
+ if (typeof x === 'string')
58
+ stack.push(x);
59
+ for (const t of okr.get(id) ?? [])
60
+ stack.push(t);
61
+ }
62
+ return out;
63
+ }
64
+ export const isDone = (n) => dataOf(n).state === 'done';
65
+ export function unfinishedIn(ids, nodes) {
66
+ const byId = new Map(nodes.map(n => [n.id, n]));
67
+ const out = [];
68
+ for (const id of ids) {
69
+ const n = byId.get(id);
70
+ if (n && !isDone(n))
71
+ out.push(id);
72
+ }
73
+ return out;
74
+ }
75
+ export function doneAtOf(n, now = Date.now()) {
76
+ const d = dataOf(n);
77
+ for (const v of [d.doneAt, d.stateChangedAt, d.updatedAt])
78
+ if (typeof v === 'number' && Number.isFinite(v) && v > 0)
79
+ return v;
80
+ return now;
81
+ }
82
+ export function fileSet(ids, releaseId, nodes, opts = {}) {
83
+ const byId = new Map(nodes.map(n => [n.id, n]));
84
+ const out = [];
85
+ for (const id of ids) {
86
+ const n = byId.get(id);
87
+ if (!n)
88
+ continue;
89
+ if (opts.onlyDone !== false && !isDone(n))
90
+ continue;
91
+ if (releaseIdOf(n) === releaseId)
92
+ continue;
93
+ out.push({ id, patch: { releaseId, doneAt: doneAtOf(n, opts.now) } });
94
+ }
95
+ return out;
96
+ }
97
+ export function unfileSet(ids, nodes) {
98
+ const byId = new Map(nodes.map(n => [n.id, n]));
99
+ const out = [];
100
+ for (const id of ids)
101
+ if (releaseIdOf(byId.get(id) ?? { id, data: {} }))
102
+ out.push({ id, patch: { releaseId: null } });
103
+ return out;
104
+ }
105
+ export function memberCounts(releaseId, nodes) {
106
+ const out = {};
107
+ for (const n of membersOf(releaseId, nodes)) {
108
+ const lvl = String(dataOf(n).hierarchyLevel ?? 'item');
109
+ out[lvl] = (out[lvl] ?? 0) + 1;
110
+ }
111
+ return out;
112
+ }
113
+ export function previousRelease(from, releases, exceptId) {
114
+ let best = null;
115
+ for (const r of releases) {
116
+ if (r.subtype !== RELEASE_SUBTYPE || r.id === exceptId)
117
+ continue;
118
+ const to = String(r.data?.to ?? '');
119
+ if (!to || to >= from)
120
+ continue;
121
+ if (!best || to > String(best.data.to ?? ''))
122
+ best = r;
123
+ }
124
+ return best;
125
+ }
126
+ const iso = (x) => `${x.getFullYear()}-${String(x.getMonth() + 1).padStart(2, '0')}-${String(x.getDate()).padStart(2, '0')}`;
127
+ export function weekWindow(d = new Date()) {
128
+ const day = (d.getDay() + 6) % 7;
129
+ const mon = new Date(d);
130
+ mon.setDate(d.getDate() - day);
131
+ const sun = new Date(mon);
132
+ sun.setDate(mon.getDate() + 6);
133
+ return { from: iso(mon), to: iso(sun) };
134
+ }
135
+ export function releaseOf(n, releases) {
136
+ const rid = releaseIdOf(n);
137
+ return (rid && releases.get(rid)) || null;
138
+ }
139
+ export function filedItemIds(nodes, releases) {
140
+ const out = new Set();
141
+ if (releases.size === 0)
142
+ return out;
143
+ for (const n of nodes)
144
+ if (isFiled(n, releases))
145
+ out.add(n.id);
146
+ return out;
147
+ }
148
+ export function releaseModulesFromGraphNodes(nodes) {
149
+ const out = new Map();
150
+ for (const raw of nodes) {
151
+ const n = raw;
152
+ if (!n || n.type !== 'rad-module' || typeof n.id !== 'string' || n.data?.subtype !== RELEASE_SUBTYPE)
153
+ continue;
154
+ const md = n.data.moduleData;
155
+ out.set(n.id, { id: n.id, subtype: RELEASE_SUBTYPE, data: md && typeof md === 'object' ? md : {} });
156
+ }
157
+ return out;
158
+ }
159
+ export function outTargetsOf(inst) {
160
+ const list = Array.isArray(inst.outConnectedNodeIds) ? inst.outConnectedNodeIds.filter(Boolean) : [];
161
+ const single = inst.outConnectedNodeId;
162
+ const ids = single && !list.includes(single) ? [single, ...list] : [...list];
163
+ return Array.from(new Set(ids));
164
+ }
165
+ const str = (v) => (typeof v === 'string' ? v : '');
166
+ export function chainWrites(id, all) {
167
+ const releases = all.filter(r => r.subtype === RELEASE_SUBTYPE);
168
+ const self = releases.find(r => r.id === id);
169
+ if (!self)
170
+ return [];
171
+ const isRelease = (x) => !!x && releases.some(r => r.id === x);
172
+ const out = new Map();
173
+ for (const r of releases) {
174
+ const t = outTargetsOf(r)[0];
175
+ out.set(r.id, isRelease(t) ? t : null);
176
+ }
177
+ const original = new Map(out);
178
+ const mine = out.get(id) ?? null;
179
+ for (const r of releases)
180
+ if (r.id !== id && out.get(r.id) === id)
181
+ out.set(r.id, mine === r.id ? null : mine);
182
+ out.set(id, null);
183
+ const from = str(self.data.from);
184
+ const prev = from ? previousRelease(from, releases, id) : null;
185
+ if (prev) {
186
+ const next = out.get(prev.id) ?? null;
187
+ out.set(prev.id, id);
188
+ out.set(id, next && next !== id ? next : null);
189
+ }
190
+ else {
191
+ const to = str(self.data.to) || from;
192
+ let next = null;
193
+ for (const r of releases) {
194
+ if (r.id === id)
195
+ continue;
196
+ const f = str(r.data.from);
197
+ if (!f || !to || f <= to)
198
+ continue;
199
+ if (!next || f < str(next.data.from))
200
+ next = r;
201
+ }
202
+ const fed = next && [...out.entries()].some(([k, v]) => k !== id && v === next.id);
203
+ if (next && !fed)
204
+ out.set(id, next.id);
205
+ }
206
+ const writes = [];
207
+ for (const [k, v] of out)
208
+ if ((original.get(k) ?? null) !== v)
209
+ writes.push({ id: k, out: v });
210
+ return writes;
211
+ }
212
+ export function chainReleasesFromGraphNodes(nodes) {
213
+ const out = [];
214
+ for (const n of nodes) {
215
+ if (n.type !== 'rad-module' || n.data?.subtype !== RELEASE_SUBTYPE)
216
+ continue;
217
+ const md = n.data.moduleData;
218
+ out.push({
219
+ id: n.id, subtype: RELEASE_SUBTYPE, data: md && typeof md === 'object' ? md : {},
220
+ ...(typeof n.data.outConnectedNodeId === 'string' ? { outConnectedNodeId: n.data.outConnectedNodeId } : {}),
221
+ ...(Array.isArray(n.data.outConnectedNodeIds) ? { outConnectedNodeIds: n.data.outConnectedNodeIds } : {}),
222
+ });
223
+ }
224
+ return out;
225
+ }
226
+ export function applyChainWrites(nodes, writes) {
227
+ if (!writes.length)
228
+ return [...nodes];
229
+ const w = new Map(writes.map(x => [x.id, x.out]));
230
+ return nodes.map(n => {
231
+ if (!w.has(n.id))
232
+ return n;
233
+ const { outConnectedNodeId: _o, outConnectedNodeIds: _l, ...rest } = n.data ?? {};
234
+ const out = w.get(n.id);
235
+ return { ...n, data: { ...rest, ...(out ? { outConnectedNodeId: out } : {}) } };
236
+ });
237
+ }
238
+ export function applyItemPatches(nodes, patches) {
239
+ if (!patches.length)
240
+ return [...nodes];
241
+ const p = new Map(patches.map(x => [x.id, x.patch]));
242
+ return nodes.map(n => (p.has(n.id) ? { ...n, data: { ...(n.data ?? {}), ...p.get(n.id) } } : n));
243
+ }
244
+ export function patchReleaseData(nodes, releaseId, patch) {
245
+ return nodes.map(n => (n.id === releaseId && n.type === 'rad-module' ? { ...n, data: { ...(n.data ?? {}), moduleData: { ...(n.data?.moduleData ?? {}), ...patch } } } : n));
246
+ }
247
+ export function newReleaseId(now = Date.now()) {
248
+ return `radmod-${RELEASE_SUBTYPE}-${now}-${Math.random().toString(36).slice(2, 8)}`;
249
+ }
250
+ const overlaps = (a, b) => Math.abs(a.x - b.x) < RELEASE_SIZE.width + 20 && Math.abs(a.y - b.y) < RELEASE_SIZE.height + 20;
251
+ export function placeRelease(nodes, from, prevId, xOfDate) {
252
+ const cards = nodes.filter(n => n.type === 'portfolio-item');
253
+ const rels = nodes.filter(n => n.type === 'rad-module' && n.data?.subtype === RELEASE_SUBTYPE);
254
+ const GAP = 80;
255
+ const topY = cards.length ? Math.min(...cards.map(n => n.position?.y ?? 0)) : 0;
256
+ const pos = { x: xOfDate(from), y: topY - RELEASE_SIZE.height - GAP };
257
+ while (rels.some(r => r.position && overlaps(r.position, pos)))
258
+ pos.y -= RELEASE_SIZE.height + 24;
259
+ const freeOf = (n) => n.freePosition ?? n.position ?? { x: 0, y: 0 };
260
+ const prev = prevId ? rels.find(r => r.id === prevId) : undefined;
261
+ let free;
262
+ if (prev)
263
+ free = { x: freeOf(prev).x + RELEASE_SIZE.width + 60, y: freeOf(prev).y };
264
+ else {
265
+ const minX = cards.length ? Math.min(...cards.map(n => freeOf(n).x)) : 0;
266
+ const minY = cards.length ? Math.min(...cards.map(n => freeOf(n).y)) : 0;
267
+ free = { x: minX, y: minY - RELEASE_SIZE.height - 120 };
268
+ }
269
+ while (rels.some(r => overlaps(freeOf(r), free)))
270
+ free.x += RELEASE_SIZE.width + 60;
271
+ return { position: pos, freePosition: free };
272
+ }
273
+ export function releaseNode(id, md, place) {
274
+ return {
275
+ id, type: 'rad-module',
276
+ position: { ...place.position },
277
+ freePosition: { ...place.freePosition },
278
+ data: { subtype: RELEASE_SUBTYPE, moduleData: { ...md }, width: RELEASE_SIZE.width, height: RELEASE_SIZE.height },
279
+ };
280
+ }
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { ApiContext } from './config.js';
3
3
  export declare const SERVER_NAME = "nonbot-mcp";
4
- export declare const SERVER_VERSION = "0.2.3";
4
+ export declare const SERVER_VERSION = "0.3.0";
5
5
  export declare const INSTRUCTIONS: string;
6
6
  export declare function registerAll(server: McpServer, ctx: ApiContext): void;
7
7
  export declare function createNonbotMcpServer(ctx: ApiContext): McpServer;
@@ -2,6 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { registerPortfolioTools } from './toolsets/portfolio.js';
3
3
  import { registerIpTools } from './toolsets/ip.js';
4
4
  import { registerBuildTools } from './toolsets/build.js';
5
+ import { registerReleaseTools } from './toolsets/release.js';
5
6
  import { registerWhygraphTools } from './toolsets/whygraph.js';
6
7
  import { registerRunTools } from './toolsets/run.js';
7
8
  import { registerMonitorTools } from './toolsets/monitor.js';
@@ -9,7 +10,7 @@ import { registerRadbugsTools } from './toolsets/radbugs.js';
9
10
  import { registerResources } from './resources.js';
10
11
  import { registerPrompts } from './prompts.js';
11
12
  export const SERVER_NAME = 'nonbot-mcp';
12
- export const SERVER_VERSION = '0.2.3';
13
+ export const SERVER_VERSION = '0.3.0';
13
14
  export const INSTRUCTIONS = [
14
15
  'non.bot MCP — work against the caller’s real product-management data, not a copy.',
15
16
  'Two domains: PORTFOLIOS (the SAFe portfolio canvas — epics → features → stories) and',
@@ -19,6 +20,9 @@ export const INSTRUCTIONS = [
19
20
  '(parents / OKR links / dependencies by id or by a key from the same call) and lays the Timeline out;',
20
21
  'portfolio_patch_item changes one item; portfolio_layout re-lays everything; portfolio_set_meta sets',
21
22
  'context text and palette; portfolio_asset_upload stores an image; ip_create adds an IP node over its sources.',
23
+ 'OWNERS: give every feature, story, enabler and milestone an owner so the human can see what is theirs —',
24
+ 'the human’s name for work only a person can do (review or approve a plan, decide, buy, sign in, hand over a key,',
25
+ 'test on a real device), owner "AI" for work an agent delivers (code, tests, migrations, docs, research).',
22
26
  'Whole-graph rewrites (restructures, deletes): portfolio_get for the graph + version, then portfolio_update',
23
27
  '(pass expectedVersion for safe concurrent editing).',
24
28
  'Before a non-trivial decision, call whygraph_context to surface the LAWs and prior',
@@ -29,11 +33,17 @@ export const INSTRUCTIONS = [
29
33
  'tokens with the explicit runs:respond scope only).',
30
34
  'IP NODES: ip_list / ip_get read a patent or trademark disclosure; ip_update edits it (op list + expected_version);',
31
35
  'ip_evidence_add adds evidence the SERVER hashes, timestamps and numbers — never send a hash or a time.',
32
- 'RADBUGS: radbug_file files a bug into the non.bot radBUGS inbox (needs the radbugs:write scope).',
36
+ 'RELEASES: portfolio_completed_work says what got done in a window (rolled up, each date naming its clock);',
37
+ 'portfolio_rebuild_done_work finds pointers (commits, closed radBUGS, comments) for done cards with no clock;',
38
+ 'portfolio_propose_releases writes DRAFT releases (files nothing) and portfolio_release_approve files a draft\u2019s cards;',
39
+ 'portfolio_release_create / _file / _unfile / _list manage releases directly. Filing never moves a card.',
40
+ 'RADBUGS: radbug_file files a bug into the non.bot radBUGS inbox (needs the radbugs:write scope); radbug_list reads',
41
+ 'them (site owner only).',
33
42
  ].join(' ');
34
43
  export function registerAll(server, ctx) {
35
44
  registerPortfolioTools(server, ctx);
36
45
  registerBuildTools(server, ctx);
46
+ registerReleaseTools(server, ctx);
37
47
  registerIpTools(server, ctx);
38
48
  registerWhygraphTools(server, ctx);
39
49
  registerRunTools(server, ctx);
@@ -0,0 +1,31 @@
1
+ import { type Level, type LayoutNode } from './layout.js';
2
+ export declare const CARD_STYLES: readonly ["plain", "poster", "split", "image-top", "cover", "grid-2", "grid-4"];
3
+ export declare const TITLE_SIZES: readonly ["sm", "md", "lg"];
4
+ export declare const ACCENTS: readonly ["tint", "bar", "plain"];
5
+ export declare const SHOW_KEYS: readonly ["bluf", "description", "acceptanceCriteria", "tags", "dates", "wsjf"];
6
+ export declare const STATES: readonly ["funnel", "reviewing", "analyzing", "ready", "implementing", "done"];
7
+ export interface StyleRule {
8
+ levels?: Level[];
9
+ states?: string[];
10
+ ids?: string[];
11
+ hasImage?: boolean;
12
+ artifacts?: boolean;
13
+ display?: {
14
+ cardStyle?: (typeof CARD_STYLES)[number];
15
+ titleSize?: (typeof TITLE_SIZES)[number];
16
+ accent?: (typeof ACCENTS)[number];
17
+ show?: Partial<Record<(typeof SHOW_KEYS)[number], boolean>>;
18
+ };
19
+ size?: {
20
+ width: number;
21
+ height: number;
22
+ } | 'auto';
23
+ satellites?: 'close';
24
+ }
25
+ export interface StyleResult<T> {
26
+ nodes: T[];
27
+ matched: number;
28
+ changed: number;
29
+ perRule: number[];
30
+ }
31
+ export declare function applyStyleRules<T extends LayoutNode>(nodes: T[], rules: StyleRule[]): StyleResult<T>;
@@ -0,0 +1,64 @@
1
+ import { LEVELS } from './layout.js';
2
+ export const CARD_STYLES = ['plain', 'poster', 'split', 'image-top', 'cover', 'grid-2', 'grid-4'];
3
+ export const TITLE_SIZES = ['sm', 'md', 'lg'];
4
+ export const ACCENTS = ['tint', 'bar', 'plain'];
5
+ export const SHOW_KEYS = ['bluf', 'description', 'acceptanceCriteria', 'tags', 'dates', 'wsjf'];
6
+ export const STATES = ['funnel', 'reviewing', 'analyzing', 'ready', 'implementing', 'done'];
7
+ const hasImage = (n) => Array.isArray(n.data?.pastedImages) && n.data.pastedImages.length > 0;
8
+ function matches(rule, n) {
9
+ if (n.type !== 'portfolio-item')
10
+ return false;
11
+ if (n.data?.childCardType && !rule.artifacts)
12
+ return false;
13
+ const level = LEVELS.includes(n.data?.hierarchyLevel) ? n.data.hierarchyLevel : 'story';
14
+ if (rule.levels && !rule.levels.includes(level))
15
+ return false;
16
+ if (rule.states && !rule.states.includes(String(n.data?.state ?? 'funnel')))
17
+ return false;
18
+ if (rule.ids && !rule.ids.includes(n.id))
19
+ return false;
20
+ if (rule.hasImage !== undefined && hasImage(n) !== rule.hasImage)
21
+ return false;
22
+ return true;
23
+ }
24
+ export function applyStyleRules(nodes, rules) {
25
+ const perRule = rules.map(() => 0);
26
+ const touched = new Set();
27
+ let changed = 0;
28
+ const out = nodes.map(n => {
29
+ let data = null;
30
+ rules.forEach((rule, i) => {
31
+ if (!matches(rule, n))
32
+ return;
33
+ perRule[i]++;
34
+ touched.add(n.id);
35
+ data ??= { ...(n.data ?? {}) };
36
+ if (rule.display) {
37
+ const prev = (data.nodeDisplay && typeof data.nodeDisplay === 'object') ? data.nodeDisplay : {};
38
+ const next = { ...prev };
39
+ if (rule.display.cardStyle)
40
+ next.cardStyle = rule.display.cardStyle;
41
+ if (rule.display.titleSize)
42
+ next.titleSize = rule.display.titleSize;
43
+ if (rule.display.accent)
44
+ next.accent = rule.display.accent;
45
+ if (rule.display.show)
46
+ next.show = { ...(prev.show ?? {}), ...rule.display.show };
47
+ data.nodeDisplay = next;
48
+ }
49
+ if (rule.size === 'auto')
50
+ delete data.canvasSize;
51
+ else if (rule.size)
52
+ data.canvasSize = { width: Math.round(rule.size.width), height: Math.round(rule.size.height) };
53
+ if (rule.satellites === 'close')
54
+ data.innerLoopExpanded = false;
55
+ });
56
+ if (!data)
57
+ return n;
58
+ if (JSON.stringify(data) === JSON.stringify(n.data ?? {}))
59
+ return n;
60
+ changed++;
61
+ return { ...n, data };
62
+ });
63
+ return { nodes: out, matched: touched.size, changed, perRule };
64
+ }
@@ -2,18 +2,18 @@ import { z } from 'zod';
2
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import type { ApiContext } from '../config.js';
4
4
  import { type LayoutNode } from '../layout.js';
5
- type Node = LayoutNode & {
5
+ export type Node = LayoutNode & {
6
6
  id: string;
7
7
  type?: string;
8
8
  data?: Record<string, any>;
9
9
  };
10
- type Edge = {
10
+ export type Edge = {
11
11
  id: string;
12
12
  source: string;
13
13
  target: string;
14
14
  [k: string]: unknown;
15
15
  };
16
- interface Graph {
16
+ export interface Graph {
17
17
  name: string;
18
18
  description: string;
19
19
  nodes: Node[];
@@ -23,6 +23,14 @@ interface Graph {
23
23
  }
24
24
  export declare const toHtml: (s: string) => string;
25
25
  export declare function newItemId(now?: number): string;
26
+ export declare function loadGraph(ctx: ApiContext, id: string): Promise<Graph>;
27
+ export declare function saveGraph(ctx: ApiContext, id: string, before: Graph, after: Graph, expectedVersion?: number): Promise<{
28
+ ok: true;
29
+ version?: number;
30
+ } | {
31
+ ok: false;
32
+ message: string;
33
+ }>;
26
34
  declare const itemSpec: z.ZodObject<{
27
35
  key: z.ZodOptional<z.ZodString>;
28
36
  level: z.ZodEnum<{
@@ -67,6 +75,11 @@ declare const itemSpec: z.ZodObject<{
67
75
  fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
68
76
  }, z.core.$strip>;
69
77
  type ItemSpec = z.infer<typeof itemSpec>;
78
+ export declare const AI_OWNER = "AI";
79
+ export declare function ownerReport(specs: Array<Pick<ItemSpec, 'key' | 'level' | 'title' | 'owner'>>): {
80
+ owners: Record<string, number>;
81
+ unassigned: string[];
82
+ };
70
83
  export declare function buildItems(g: Graph, specs: ItemSpec[], now?: Date): {
71
84
  nodes: Node[];
72
85
  edges: Edge[];