@supermux/core 0.16.0 → 0.16.1

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.
@@ -9,12 +9,12 @@
9
9
  * into place, and the links made here (a plugin, a skills folder) are new: making one never
10
10
  * follows one.
11
11
  */
12
- import { lstatSync, readFileSync, readdirSync, readlinkSync, rmSync, symlinkSync } from "node:fs";
12
+ import { lstatSync, readFileSync, readdirSync, readlinkSync, rmSync } from "node:fs";
13
13
  import { basename, join } from "node:path";
14
14
  import { mappedPluginDrops, mappedPluginServers, readPlugin, writeSkillsPlugin } from "./plugins.js";
15
15
  import { reloadOnlyUpdates } from "./update.js";
16
16
  import { writeAntigravityMcpConfig, writeAntigravityPlugins } from "../environment/antigravity.js";
17
- import { assertRealDirectory, realDirectoryIn, writePrivateFile } from "../private-files.js";
17
+ import { assertRealDirectory, linkDirectory, realDirectoryIn, writePrivateFile } from "../private-files.js";
18
18
  const dropped = (context, kind, item) => context.dropped.some(drop => drop.kind === kind && drop.item === item);
19
19
  /** What a launch context applies: its items minus the ones the core dropped. */
20
20
  export const appliedContext = (context) => applied(context);
@@ -42,7 +42,7 @@ function pluginFolder(context, skills, plugins, manifests) {
42
42
  const entries = [];
43
43
  plugins.forEach((path, index) => {
44
44
  const link = join(folder, `${String(index + 1).padStart(2, "0")}-${basename(path)}`);
45
- symlinkSync(path, link);
45
+ linkDirectory(path, link);
46
46
  entries.push(link);
47
47
  });
48
48
  skills.forEach((root, index) => {
@@ -96,7 +96,7 @@ export function editPluginFolder(directory, changes, manifests) {
96
96
  rmSync(path, { recursive: true, force: true });
97
97
  undo.push(() => {
98
98
  if (entry.kind === "plugins")
99
- symlinkSync(entry.path, path);
99
+ linkDirectory(entry.path, path);
100
100
  else
101
101
  writeSkillsPlugin(path, entry.name, entry.path, manifests);
102
102
  });
@@ -104,8 +104,9 @@ export function editPluginFolder(directory, changes, manifests) {
104
104
  }
105
105
  else if (change.kind === "plugins") {
106
106
  const path = join(folder, `${String(++pluginIndex).padStart(2, "0")}-${basename(change.item)}`);
107
- symlinkSync(change.item, path);
108
- undo.push(() => rmSync(path, { force: true }));
107
+ linkDirectory(change.item, path);
108
+ // recursive: Bun on Windows refuses a plain rm of a junction (EFAULT); it still only removes the link.
109
+ undo.push(() => rmSync(path, { recursive: true, force: true }));
109
110
  }
110
111
  else {
111
112
  const name = `supermux-skills-${++skillsIndex}`;
@@ -1,6 +1,6 @@
1
- import { existsSync, readFileSync, readdirSync, statSync, symlinkSync } from "node:fs";
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
2
  import { basename, dirname, join } from "node:path";
3
- import { assertRealDirectory, realDirectoryIn, writePrivateFile } from "../private-files.js";
3
+ import { assertRealDirectory, linkDirectory, realDirectoryIn, writePrivateFile } from "../private-files.js";
4
4
  import { MCP_SERVER_NAME } from "./index.js";
5
5
  function isDirectory(path) {
6
6
  try {
@@ -127,6 +127,6 @@ export function writeSkillsPlugin(dir, name, skillsRoot, manifests) {
127
127
  ...(manifest === ".cursor-plugin" ? { skills: "./skills/" } : {}),
128
128
  }, null, 2) + "\n", () => assertRealDirectory(folder));
129
129
  }
130
- symlinkSync(skillsRoot, join(wrapper, "skills"));
130
+ linkDirectory(skillsRoot, join(wrapper, "skills"));
131
131
  return wrapper;
132
132
  }
@@ -1,9 +1,9 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { randomBytes } from 'node:crypto';
3
- import { lstatSync, mkdirSync, realpathSync } from 'node:fs';
3
+ import { lstatSync, mkdirSync, readdirSync, realpathSync } from 'node:fs';
4
4
  import { lstat, rm, unlink } from 'node:fs/promises';
5
5
  import { createConnection } from 'node:net';
6
- import { dirname, join, resolve } from 'node:path';
6
+ import { basename, dirname, join, resolve } from 'node:path';
7
7
  import { CoreError } from '../errors.js';
8
8
  import { assertNoLinksIn, assertRealDirectory, present, readPrivateFile, realDirectoryIn, writePrivateFile } from '../private-files.js';
9
9
  import { CORE_SUBCOMMANDS, selfEntry } from '../self-entry.js';
@@ -114,6 +114,8 @@ function mine(info) {
114
114
  * reboot emptied it, say) and listen there, to be sent a session's token.
115
115
  */
116
116
  function trustedSocket(dir, path) {
117
+ if (process.platform === 'win32')
118
+ return listedSocket(path);
117
119
  try {
118
120
  const socket = lstatSync(path);
119
121
  if (!socket.isSocket() || !mine(socket))
@@ -127,12 +129,29 @@ function trustedSocket(dir, path) {
127
129
  return false;
128
130
  }
129
131
  }
132
+ /**
133
+ * Windows: an AF_UNIX socket file is listed in its folder but cannot be stat'ed (EACCES), and a uid or
134
+ * mode would mean nothing there; its folders (the keeper's, the user's own temp dir) are the user's.
135
+ */
136
+ function listedSocket(path) {
137
+ try {
138
+ return readdirSync(dirname(path)).includes(basename(path));
139
+ }
140
+ catch {
141
+ return false;
142
+ }
143
+ }
130
144
  /** The socket a new keeper of `dir` listens on: the first candidate whose directory this user can make private. */
131
145
  function newSocketPath(dir) {
132
146
  return privateSocketPath(keeperSocketPaths(dir), join(dir, 'keeper.sock'));
133
147
  }
134
148
  /** Deletes the socket a keeper left at `path` when it is this user's; anything else there stays (the path comes from a status file). */
135
149
  async function removeSocket(path) {
150
+ if (process.platform === 'win32') {
151
+ if (listedSocket(path))
152
+ await unlink(path).catch(() => { });
153
+ return;
154
+ }
136
155
  try {
137
156
  const info = await lstat(path);
138
157
  if (info.isSocket() && mine(info))
@@ -421,6 +440,7 @@ async function connectLocked(options) {
421
440
  const child = spawn(entry.command, entry.args, {
422
441
  detached: true,
423
442
  stdio: 'ignore',
443
+ windowsHide: true,
424
444
  cwd: entry.cwd,
425
445
  env: {
426
446
  ...process.env,
@@ -254,6 +254,8 @@ async function main() {
254
254
  env: agentEnv,
255
255
  stdio: ['pipe', 'pipe', 'pipe'],
256
256
  shell: false,
257
+ // Windows: the keeper has no console, so a console agent (claude.exe) would get a visible one of its own.
258
+ windowsHide: true,
257
259
  });
258
260
  if (!captureStderr)
259
261
  agent.stderr?.resume();
@@ -6,6 +6,11 @@ export declare function realDirectoryIn(parent: string, name: string): string;
6
6
  export declare function realStateDirectory(path: string): string;
7
7
  /** Refuses a link, or anything of another user, among the entries of `dir`. */
8
8
  export declare function assertNoLinksIn(dir: string): void;
9
+ /**
10
+ * A new link at `path` to directory `target`. On Windows a junction: a symbolic link there needs
11
+ * Developer Mode or an elevated process (EPERM otherwise), a junction needs neither.
12
+ */
13
+ export declare function linkDirectory(target: string, path: string, platform?: NodeJS.Platform): void;
9
14
  /** Whether `path` names anything (a link included), without following it. */
10
15
  export declare function present(path: string): boolean;
11
16
  /** A new 0600 file of this user's at `path`, open with `flags`: never an existing file, never through a link. */
@@ -6,7 +6,7 @@
6
6
  * (O_CREAT | O_EXCL, 0600) and renamed into place, so an existing name is never opened for writing.
7
7
  */
8
8
  import { randomBytes } from "node:crypto";
9
- import { closeSync, constants, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeSync } from "node:fs";
9
+ import { closeSync, constants, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, symlinkSync, unlinkSync, writeSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
  import { CoreError } from "./errors.js";
12
12
  /** Windows has no O_NOFOLLOW (nor keepers): there the lstat checks remain. */
@@ -64,6 +64,9 @@ export function assertNoLinksIn(dir) {
64
64
  catch (error) {
65
65
  if (code(error) === "ENOENT")
66
66
  continue;
67
+ // A Windows AF_UNIX socket (a keeper's) cannot be stat'ed; a link or junction can.
68
+ if (code(error) === "EACCES" && process.platform === "win32")
69
+ continue;
67
70
  throw error;
68
71
  }
69
72
  if (info.isSymbolicLink())
@@ -72,6 +75,13 @@ export function assertNoLinksIn(dir) {
72
75
  throw refuse(path, "it belongs to another user");
73
76
  }
74
77
  }
78
+ /**
79
+ * A new link at `path` to directory `target`. On Windows a junction: a symbolic link there needs
80
+ * Developer Mode or an elevated process (EPERM otherwise), a junction needs neither.
81
+ */
82
+ export function linkDirectory(target, path, platform = process.platform) {
83
+ symlinkSync(target, path, platform === "win32" ? "junction" : "dir");
84
+ }
75
85
  /** Whether `path` names anything (a link included), without following it. */
76
86
  export function present(path) {
77
87
  try {
package/dist/version.d.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * The version of this package, announced to agents as the `clientInfo` version of their handshake.
3
3
  * Keep it equal to `version` in package.json: tests/alias.test.ts fails when they differ.
4
4
  */
5
- export declare const LIBRARY_VERSION = "0.16.0";
5
+ export declare const LIBRARY_VERSION = "0.16.1";
package/dist/version.js CHANGED
@@ -2,4 +2,4 @@
2
2
  * The version of this package, announced to agents as the `clientInfo` version of their handshake.
3
3
  * Keep it equal to `version` in package.json: tests/alias.test.ts fails when they differ.
4
4
  */
5
- export const LIBRARY_VERSION = "0.16.0";
5
+ export const LIBRARY_VERSION = "0.16.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supermux/core",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "type": "module",
5
5
  "description": "Build your own app on Claude Code, Codex, Cursor, OpenCode, Grok Build and Antigravity: one TypeScript API for sessions, events and permission prompts.",
6
6
  "keywords": [