bantamkit-mcp 0.35.8 → 0.35.9

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.
@@ -79,9 +79,55 @@ export class ArgumentTypeError extends Error {
79
79
  * a second hand-written copy of a conversion rule is the defect this module was built from.
80
80
  */
81
81
  export function pyIntStrict(text) {
82
- if (!/^\s*[+-]?\d+(?:_\d+)*\s*$/.test(text))
82
+ // job66/#174: CPython's `int(str)`, measured on 3.12 rather than recalled. It first maps
83
+ // every NON-ASCII character through `_PyUnicode_TransformDecimalAndSpaceToASCII`: a Unicode
84
+ // decimal digit (category Nd, any script, astral included) becomes its ASCII digit and a
85
+ // Unicode space becomes ' '. ASCII passes through untouched, so the only ASCII whitespace
86
+ // the parser then skips is ` \t\n\v\f\r` — `int('3\x1c')` is a ValueError although
87
+ // `'\x1c'.isspace()` is true — and U+FEFF is not a space at all. Single underscores may sit
88
+ // BETWEEN digits only, and more than 4300 digits is a ValueError (`sys.int_info
89
+ // .default_max_str_digits`). WAS: `/^\s*[+-]?\d+(?:_\d+)*\s*$/` without the `u` flag —
90
+ // ASCII digits only, so `--k ٣` was exit 2 here and served on the reference, while JS
91
+ // `\s` admitted U+FEFF and refused U+0085, both the opposite of CPython.
92
+ let ascii = '';
93
+ for (const ch of text) {
94
+ const cp = ch.codePointAt(0);
95
+ if (cp < 0x80)
96
+ ascii += ch;
97
+ else if (PY_UNICODE_SPACE.has(cp))
98
+ ascii += ' ';
99
+ else if (/\p{Nd}/u.test(ch))
100
+ ascii += String(ndValue(cp));
101
+ else
102
+ throw new TypeError('not an int');
103
+ }
104
+ const match = /^[ \t\n\v\f\r]*([+-]?)([0-9]+(?:_[0-9]+)*)[ \t\n\v\f\r]*$/.exec(ascii);
105
+ if (match === null)
83
106
  throw new TypeError('not an int');
84
- return Number(text.trim().replace(/_/g, ''));
107
+ const digits = match[2].replace(/_/g, '');
108
+ if (digits.length > PY_INT_MAX_STR_DIGITS)
109
+ throw new TypeError('not an int');
110
+ const value = Number(`${match[1]}${digits}`);
111
+ return value === 0 ? 0 : value; // `int('-0')` is 0, never JavaScript's -0
112
+ }
113
+ /** `sys.int_info.default_max_str_digits`: CPython refuses a longer decimal string. */
114
+ const PY_INT_MAX_STR_DIGITS = 4300;
115
+ /** The non-ASCII code points `str.isspace()` is true for (CPython 3.12, Unicode 15.0). */
116
+ const PY_UNICODE_SPACE = new Set([
117
+ 0x85, 0xa0, 0x1680, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008,
118
+ 0x2009, 0x200a, 0x2028, 0x2029, 0x202f, 0x205f, 0x3000,
119
+ ]);
120
+ /**
121
+ * `unicodedata.decimal(ch)` for a category-Nd code point. Unicode allocates every decimal
122
+ * digit set as ten contiguous code points from 0 to 9, and sets that touch are back to back
123
+ * (the five mathematical sets at U+1D7CE), so the value is the offset from the start of the
124
+ * contiguous Nd run, modulo ten.
125
+ */
126
+ function ndValue(cp) {
127
+ let start = cp;
128
+ while (start > 0 && /\p{Nd}/u.test(String.fromCodePoint(start - 1)))
129
+ start -= 1;
130
+ return (cp - start) % 10;
85
131
  }
86
132
  /** `_get_action_name`: the name argparse puts after `argument `. */
87
133
  const actionName = (action) => action.optionStrings.length > 0
@@ -31,7 +31,7 @@
31
31
  * total`), never as a comparison afterwards: a `number` past 2**53 is already rounded, so the
32
32
  * check would be reading a value this side had got wrong.
33
33
  */
34
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
34
+ import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from 'node:fs';
35
35
  import { join } from 'node:path';
36
36
  import { BantamError } from './errors.js';
37
37
  import { MAX_SAFE_INT, TOKEN_CLASSES, loadPriceTable, priceTokens, } from './pricing.js';
@@ -125,8 +125,21 @@ export function walk(base) {
125
125
  // exactly as it is there.
126
126
  isDirectory = false;
127
127
  }
128
- if (isDirectory)
129
- visit(child, `${rel}/`);
128
+ if (isDirectory) {
129
+ // #151: a symlinked directory is not a transcript tree. `statSync` follows the link,
130
+ // so a link to a directory lands in this arm and is skipped here: neither walked nor
131
+ // read. Following it walked a link to an ancestor until ELOOP and counted the one
132
+ // transcript beneath it once per level. `skill_audit`'s rule, and the reference's.
133
+ let link = false;
134
+ try {
135
+ link = lstatSync(child).isSymbolicLink();
136
+ }
137
+ catch {
138
+ link = false;
139
+ }
140
+ if (!link)
141
+ visit(child, `${rel}/`);
142
+ }
130
143
  else if (name.endsWith(TRANSCRIPT_SUFFIX))
131
144
  found.push([child, rel]);
132
145
  }
@@ -139,7 +152,10 @@ export function walk(base) {
139
152
  * raises, which would make an unreadable transcript a counted one on this side alone.
140
153
  */
141
154
  function readUtf8Strict(path) {
142
- const decoder = new TextDecoder('utf-8', { fatal: true });
155
+ // `ignoreBOM: true` KEEPS a leading U+FEFF (#150). The default strips it from the first line
156
+ // only, where `read_text(encoding="utf-8")` keeps it and `json.loads` refuses it; kept, the
157
+ // line is `unparsed-line` on both sides, as a BOM on any later line already was.
158
+ const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
143
159
  return decoder.decode(readFileSync(path));
144
160
  }
145
161
  function usageOf(record) {
@@ -30,6 +30,18 @@ export declare const UPDATE_NEVER = "update: never checked.";
30
30
  * falsify the claim, the same reason `selfupdate.COMPARISON` carries both.
31
31
  */
32
32
  export declare const UPDATE_AVAILABLE: string;
33
+ /**
34
+ * The command `UPDATE_AVAILABLE` tells the operator to run: THIS node, absolute (job66/#171).
35
+ *
36
+ * WAS: `` `{program} --update` ``, the bare command, which `--install` puts only in
37
+ * `<prefix>/node_modules/.bin` — on nobody's PATH — so the sentence answered `command not
38
+ * found` from a GUI-launched host's shell. `process.execPath` and this build's own `cli.js`
39
+ * (the sibling of this module in `dist/`), each double-quoted, need no PATH and hold a home
40
+ * directory with a space in it. The reference fills in `sys.executable -m bantamkit.mcpserver`
41
+ * instead; the TEMPLATE stays byte-identical and the filling is the `docs/porting.md` row
42
+ * *the update command names each runtime's own interpreter*.
43
+ */
44
+ export declare function updateCommand(): string;
33
45
  /** Names the date because the record may be months old. */
34
46
  export declare const UPDATE_CURRENT = "update: {program} {installed} is current as of {date}.";
35
47
  /**
@@ -111,8 +123,8 @@ export declare function readRecord(path?: string): Record<string, unknown> | nul
111
123
  * and not only on the `current` path: a record that cannot say when it was written is not a
112
124
  * record, and one rule is one thing for the two runtimes to reproduce instead of two.
113
125
  */
114
- export declare function decide(installed: string, source: Source, record: Record<string, unknown> | null, key?: string): UpdateStatus;
126
+ export declare function decide(installed: string, source: Source, record: Record<string, unknown> | null, key?: string, command?: string): UpdateStatus;
115
127
  /** Read the record and decide. The whole reader, in one call, for `bantamkit_status`. */
116
- export declare function updateStatus(installed: string, key?: string, path?: string): UpdateStatus;
128
+ export declare function updateStatus(installed: string, key?: string, path?: string, command?: string): UpdateStatus;
117
129
  /** Exactly one of the five sentences. Always a line: there is no silent state here. */
118
- export declare function updateLine(installed: string, key?: string, path?: string): string;
130
+ export declare function updateLine(installed: string, key?: string, path?: string, command?: string): string;
@@ -35,7 +35,7 @@
35
35
  * `0.10.0` is the pair that says so, and `test/updatecheck.test.mjs` both runs it and pins
36
36
  * structurally that nothing comparator-shaped is defined here.
37
37
  *
38
- * NO NEW DEPENDENCY: `node:fs`, `node:os` and `node:path`. The `str.format` substitution the
38
+ * NO NEW DEPENDENCY: `node:fs`, `node:os`, `node:path` and `node:url`. The `str.format` substitution the
39
39
  * reference gets from CPython is the four lines of `fill` below — `selfupdate.ts` has the same
40
40
  * four and cannot be imported for them, for the reason two paragraphs up.
41
41
  *
@@ -45,6 +45,7 @@
45
45
  import { readFileSync } from 'node:fs';
46
46
  import { homedir } from 'node:os';
47
47
  import { join } from 'node:path';
48
+ import { fileURLToPath } from 'node:url';
48
49
  import { compareVersions } from './npminstall.js';
49
50
  export { compareVersions };
50
51
  /**
@@ -80,7 +81,21 @@ export const UPDATE_NEVER = 'update: never checked.';
80
81
  * falsify the claim, the same reason `selfupdate.COMPARISON` carries both.
81
82
  */
82
83
  export const UPDATE_AVAILABLE = 'update: {program} {installed} is running; the package index has {latest} — run ' +
83
- '`{program} --update`, then reconnect the host.';
84
+ '`{command}`, then reconnect the host.';
85
+ /**
86
+ * The command `UPDATE_AVAILABLE` tells the operator to run: THIS node, absolute (job66/#171).
87
+ *
88
+ * WAS: `` `{program} --update` ``, the bare command, which `--install` puts only in
89
+ * `<prefix>/node_modules/.bin` — on nobody's PATH — so the sentence answered `command not
90
+ * found` from a GUI-launched host's shell. `process.execPath` and this build's own `cli.js`
91
+ * (the sibling of this module in `dist/`), each double-quoted, need no PATH and hold a home
92
+ * directory with a space in it. The reference fills in `sys.executable -m bantamkit.mcpserver`
93
+ * instead; the TEMPLATE stays byte-identical and the filling is the `docs/porting.md` row
94
+ * *the update command names each runtime's own interpreter*.
95
+ */
96
+ export function updateCommand() {
97
+ return `"${process.execPath}" "${fileURLToPath(new URL('./cli.js', import.meta.url))}" --update`;
98
+ }
84
99
  /** Names the date because the record may be months old. */
85
100
  export const UPDATE_CURRENT = 'update: {program} {installed} is current as of {date}.';
86
101
  /**
@@ -268,7 +283,7 @@ function checkedDate(record, key) {
268
283
  * and not only on the `current` path: a record that cannot say when it was written is not a
269
284
  * record, and one rule is one thing for the two runtimes to reproduce instead of two.
270
285
  */
271
- export function decide(installed, source, record, key = KEY) {
286
+ export function decide(installed, source, record, key = KEY, command = updateCommand()) {
272
287
  if (source === SOURCE_ABSENT)
273
288
  return { state: STATE_NEVER, line: UPDATE_NEVER };
274
289
  if (source !== SOURCE_RECORD || record === null) {
@@ -283,7 +298,7 @@ export function decide(installed, source, record, key = KEY) {
283
298
  if (order < 0) {
284
299
  return {
285
300
  state: STATE_AVAILABLE,
286
- line: fill(UPDATE_AVAILABLE, { program: PROGRAM, installed, latest }),
301
+ line: fill(UPDATE_AVAILABLE, { program: PROGRAM, installed, latest, command }),
287
302
  };
288
303
  }
289
304
  if (order === 0) {
@@ -298,11 +313,11 @@ export function decide(installed, source, record, key = KEY) {
298
313
  };
299
314
  }
300
315
  /** Read the record and decide. The whole reader, in one call, for `bantamkit_status`. */
301
- export function updateStatus(installed, key = KEY, path = recordPath()) {
316
+ export function updateStatus(installed, key = KEY, path = recordPath(), command = updateCommand()) {
302
317
  const { source, record } = loadRecord(path);
303
- return decide(installed, source, record, key);
318
+ return decide(installed, source, record, key, command);
304
319
  }
305
320
  /** Exactly one of the five sentences. Always a line: there is no silent state here. */
306
- export function updateLine(installed, key = KEY, path = recordPath()) {
307
- return updateStatus(installed, key, path).line;
321
+ export function updateLine(installed, key = KEY, path = recordPath(), command = updateCommand()) {
322
+ return updateStatus(installed, key, path, command).line;
308
323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bantamkit-mcp",
3
- "version": "0.35.8",
3
+ "version": "0.35.9",
4
4
  "description": "Memory MCP server for Claude Code, Cursor, VS Code Copilot and Claude Desktop: per-person memory, JSON Schema validation, shift-work tools. Pure Node; install once with npx, run offline.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -60,6 +60,6 @@
60
60
  "typescript": "^5.7.2"
61
61
  },
62
62
  "dependencies": {
63
- "@modelcontextprotocol/sdk": "1.30.0"
63
+ "@modelcontextprotocol/sdk": "1.31.0"
64
64
  }
65
65
  }