agentic-numerical-control 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Edward Coventry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,11 +1,15 @@
1
- // Start the app when the user logs in: Task Scheduler on Windows, a launchd
2
- // agent on macOS, a systemd user service on Linux. Each runs `<cli> start --log`.
3
- import { execFileSync } from "node:child_process";
1
+ // Start the app when the user logs in, without admin rights: the per-user Run
2
+ // key on Windows, a launchd agent on macOS, a systemd user service on Linux.
3
+ // Each runs `<cli> start --log`.
4
+ import { execFileSync, spawn } from "node:child_process";
4
5
  import fs from "node:fs";
5
6
  import os from "node:os";
6
7
  import path from "node:path";
8
+ import { STATE_DIR } from "./paths.js";
7
9
  import { PRODUCT } from "./product.js";
8
10
  const LABEL = `com.${PRODUCT.name}.agent`;
11
+ const RUN_KEY = String.raw `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`;
12
+ const LAUNCHER = path.join(STATE_DIR, "start.cmd");
9
13
  function quiet(file, args) {
10
14
  try {
11
15
  execFileSync(file, args, { stdio: "ignore" });
@@ -20,13 +24,18 @@ function command(cliPath) {
20
24
  export function enableAutostart(cliPath) {
21
25
  const cmd = command(cliPath);
22
26
  if (process.platform === "win32") {
23
- // conhost --headless runs the console app without a visible window.
24
- const tr = `conhost.exe --headless ${cmd.map((c) => `"${c}"`).join(" ")}`;
25
- execFileSync("schtasks", ["/Create", "/F", "/TN", PRODUCT.displayName, "/SC", "ONLOGON", "/RL", "LIMITED", "/TR", tr], {
27
+ // A short launcher keeps the Run entry well under its length limit;
28
+ // conhost --headless runs it without a visible window. Logon-triggered
29
+ // scheduled tasks would need admin rights, the Run key does not.
30
+ fs.mkdirSync(STATE_DIR, { recursive: true });
31
+ fs.writeFileSync(LAUNCHER, `@${cmd.map((c) => `"${c}"`).join(" ")}\r\n`);
32
+ execFileSync("reg", ["add", RUN_KEY, "/v", PRODUCT.displayName, "/t", "REG_SZ", "/d", `conhost.exe --headless "${LAUNCHER}"`, "/f"], {
26
33
  stdio: "ignore",
27
34
  });
28
- execFileSync("schtasks", ["/Run", "/TN", PRODUCT.displayName], { stdio: "ignore" });
29
- return `Task Scheduler task "${PRODUCT.displayName}" (runs at login)`;
35
+ // Start now as well. A detached conhost exits at once (it has no console to
36
+ // host), so run Node itself detached; it needs no console and logs to file.
37
+ spawn(cmd[0], cmd.slice(1), { detached: true, stdio: "ignore", windowsHide: true }).unref();
38
+ return "Windows sign-in startup entry (no admin needed)";
30
39
  }
31
40
  if (process.platform === "darwin") {
32
41
  const plist = path.join(os.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
@@ -63,8 +72,8 @@ WantedBy=default.target
63
72
  }
64
73
  export function disableAutostart() {
65
74
  if (process.platform === "win32") {
66
- quiet("schtasks", ["/End", "/TN", PRODUCT.displayName]);
67
- quiet("schtasks", ["/Delete", "/F", "/TN", PRODUCT.displayName]);
75
+ quiet("reg", ["delete", RUN_KEY, "/v", PRODUCT.displayName, "/f"]);
76
+ fs.rmSync(LAUNCHER, { force: true });
68
77
  }
69
78
  else if (process.platform === "darwin") {
70
79
  const plist = path.join(os.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
@@ -23,6 +23,19 @@ import { createServer } from "./server.js";
23
23
  import { updateIfAvailable } from "./update.js";
24
24
  const TOKEN_FILE = path.join(STATE_DIR, "token");
25
25
  const LOG_FILE = path.join(STATE_DIR, "agent.log");
26
+ const PID_FILE = path.join(STATE_DIR, "agent.pid");
27
+ /** Stops a background copy started at login or by an earlier setup, if running. */
28
+ function stopRunning() {
29
+ try {
30
+ const pid = Number(fs.readFileSync(PID_FILE, "utf-8"));
31
+ if (pid && pid !== process.pid)
32
+ process.kill(pid);
33
+ }
34
+ catch {
35
+ // not running
36
+ }
37
+ fs.rmSync(PID_FILE, { force: true });
38
+ }
26
39
  const CLI_PATH = fileURLToPath(import.meta.url);
27
40
  /**
28
41
  * The local HTTP endpoint is served only at /mcp/<token>, so other programs
@@ -98,10 +111,12 @@ const [command = "start", ...rest] = process.argv.slice(2);
98
111
  switch (command) {
99
112
  case "setup": {
100
113
  console.log(`\n ${PRODUCT.displayName} ${VERSION}`);
101
- if (!readLink())
102
- await pair(PRODUCT.cloudUrl);
103
- else
114
+ const link = readLink();
115
+ if (link?.cloudUrl === PRODUCT.cloudUrl)
104
116
  console.log(" This PC is already linked.");
117
+ else
118
+ await pair(PRODUCT.cloudUrl);
119
+ stopRunning();
105
120
  const how = enableAutostart(CLI_PATH);
106
121
  console.log(` Running now, and at every login (${how}).`);
107
122
  console.log(` Put G-code files in: ${JOBS_DIR}`);
@@ -111,6 +126,8 @@ switch (command) {
111
126
  case "start": {
112
127
  if (rest.includes("--log"))
113
128
  logToFile();
129
+ fs.mkdirSync(STATE_DIR, { recursive: true });
130
+ fs.writeFileSync(PID_FILE, String(process.pid));
114
131
  if (await updateIfAvailable())
115
132
  process.exit(0);
116
133
  linkCncjs();
@@ -129,6 +146,7 @@ switch (command) {
129
146
  }
130
147
  case "unlink": {
131
148
  disableAutostart();
149
+ stopRunning();
132
150
  fs.rmSync(DEVICE_FILE, { force: true });
133
151
  console.log(`Unlinked this PC and stopped ${PRODUCT.displayName} starting at login.`);
134
152
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-numerical-control",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Agentic Numerical Control: see and run CNC jobs from your AI assistant. A 3D toolpath viewer, live machine status and job control for CNCjs/GRBL machines.",
5
5
  "scripts": {
6
6
  "build": "tsc --noEmit -p tsconfig.app.json && cross-env INPUT=viewer.html vite build && tsc -p tsconfig.build.json",
@@ -64,5 +64,9 @@
64
64
  "claude",
65
65
  "gcode",
66
66
  "toolpath"
67
- ]
67
+ ],
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/EdwardCoventry/agentic-numerical-control.git"
71
+ }
68
72
  }