stikfix 1.5.0 → 1.6.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.
@@ -30,6 +30,8 @@ var REG_CHROME_KEY = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${NA
30
30
  var REG_EDGE_KEY = `HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`;
31
31
  var REG_FIREFOX_KEY = `HKCU\\Software\\Mozilla\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`;
32
32
  var DEFAULT_GECKO_ID = "stikfix@stikfix.com";
33
+ var CONFIG_DIR = (home) => (0, import_node_path2.join)(home, ".config", "stikfix");
34
+ var CONFIG_PATH = (home) => (0, import_node_path2.join)(CONFIG_DIR(home), "config.json");
33
35
  var LAUNCHER_BATCH_FILENAME = "stikfix-host.bat";
34
36
  var LAUNCHER_VBS_FILENAME = "stikfix-host.vbs";
35
37
  var LAUNCHER_LNK_FILENAME = "Stikfix Host.lnk";
@@ -176,14 +178,17 @@ function createLauncherFiles(opts) {
176
178
  const hostEntry = opts.hostEntryPath;
177
179
  const rootArg = opts.root;
178
180
  const portArg = opts.port !== void 0 ? ` --port ${opts.port}` : "";
181
+ const useExe = typeof opts.hostExe === "string" && opts.hostExe.length > 0;
182
+ const hostExe = opts.hostExe ?? "";
179
183
  if (plat === "win32") {
184
+ const batchLaunch = useExe ? `"${hostExe}" serve --root "${rootArg}"${portArg}` : `"${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`;
180
185
  const batchContent = [
181
186
  "@echo off",
182
187
  `rem Stikfix HTTP host launcher \u2014 double-click to start the backend`,
183
188
  `rem Generated by: npx stikfix init`,
184
189
  `rem Root: ${rootArg}`,
185
190
  ``,
186
- `"${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`,
191
+ batchLaunch,
187
192
  `if %ERRORLEVEL% NEQ 0 pause`
188
193
  ].join("\r\n");
189
194
  const batchPath = paths.launcher;
@@ -191,17 +196,20 @@ function createLauncherFiles(opts) {
191
196
  written.push(batchPath);
192
197
  const vq = (s) => s.replace(/"/g, '""');
193
198
  const vbsHostEntry = vq(hostEntry);
199
+ const vbsHostExe = vq(hostExe);
194
200
  const vbsRoot = vq(rootArg);
195
201
  const vbsPortArg = opts.port !== void 0 ? ` --port ${opts.port}` : "";
202
+ const vbsRunLine = useExe ? `sh.Run "cmd /c """ & hostExe & """ serve --root """ & root & """${vbsPortArg}", 0, False` : `sh.Run "cmd /c node """ & hostEntry & """ --root """ & root & """${vbsPortArg}", 0, False`;
196
203
  const vbsContent = [
197
204
  "Option Explicit",
198
- "Dim sh, fso, hostEntry, root, portFile, msg, port, f",
205
+ "Dim sh, fso, hostEntry, hostExe, root, portFile, msg, port, f",
199
206
  'Set sh = CreateObject("WScript.Shell")',
200
207
  'Set fso = CreateObject("Scripting.FileSystemObject")',
201
208
  `hostEntry = "${vbsHostEntry}"`,
209
+ `hostExe = "${vbsHostExe}"`,
202
210
  `root = "${vbsRoot}"`,
203
211
  "' Launch the host hidden (window style 0), do not wait",
204
- `sh.Run "cmd /c node """ & hostEntry & """ --root """ & root & """${vbsPortArg}", 0, False`,
212
+ vbsRunLine,
205
213
  "' Give it a moment to bind and write the port file",
206
214
  "WScript.Sleep 1800",
207
215
  'msg = "Stikfix host is running." & vbCrLf & vbCrLf & "You can start dropping notes."',
@@ -218,21 +226,22 @@ function createLauncherFiles(opts) {
218
226
  (0, import_node_fs.writeFileSync)(vbsPath, vbsContent, { encoding: "utf8" });
219
227
  written.push(vbsPath);
220
228
  if (paths.shortcut) {
221
- const lnkPath = paths.shortcut;
229
+ const guessedLnkPath = paths.shortcut;
222
230
  const iconPath = opts.iconPath ?? "";
223
- const safeLnkPath = lnkPath.replace(/'/g, "''");
231
+ const safeLnkFileName = LAUNCHER_LNK_FILENAME.replace(/'/g, "''");
224
232
  const safeIconPath = iconPath.replace(/'/g, "''");
225
233
  const wscriptPath = (0, import_node_path2.join)(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "wscript.exe");
226
234
  const safeWscript = wscriptPath.replace(/'/g, "''");
227
235
  const safeVbsPath = vbsPath.replace(/'/g, "''");
228
- const psScript = `$ws = New-Object -ComObject WScript.Shell;$s = $ws.CreateShortcut('${safeLnkPath}');$s.TargetPath = '${safeWscript}';$s.Arguments = '"${safeVbsPath}"';$s.Description = 'Start the Stikfix HTTP backend host';` + (safeIconPath ? `$s.IconLocation = '${safeIconPath},0';` : "") + `$s.Save()`;
236
+ const psScript = `$desktop = [Environment]::GetFolderPath('Desktop');$lnkPath = Join-Path $desktop '${safeLnkFileName}';$ws = New-Object -ComObject WScript.Shell;$s = $ws.CreateShortcut($lnkPath);$s.TargetPath = '${safeWscript}';$s.Arguments = '"${safeVbsPath}"';$s.Description = 'Start the Stikfix HTTP backend host';` + (safeIconPath ? `$s.IconLocation = '${safeIconPath},0';` : "") + `$s.Save();Write-Output $lnkPath`;
229
237
  try {
230
- (0, import_node_child_process.execFileSync)(
238
+ const stdout = (0, import_node_child_process.execFileSync)(
231
239
  "powershell.exe",
232
240
  ["-NoProfile", "-NonInteractive", "-Command", psScript],
233
- { timeout: 15e3, stdio: "ignore" }
241
+ { timeout: 15e3, encoding: "utf8" }
234
242
  );
235
- written.push(lnkPath);
243
+ const resolvedLnkPath = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop() ?? guessedLnkPath;
244
+ written.push(resolvedLnkPath);
236
245
  } catch (err) {
237
246
  warnings.push(
238
247
  `Desktop shortcut creation failed (non-fatal): ${String(err.message)}. The batch file at ${batchPath} can be used directly.`
@@ -245,7 +254,7 @@ function createLauncherFiles(opts) {
245
254
  `# Stikfix HTTP host launcher \u2014 double-click in Finder or drag to Dock`,
246
255
  `# Generated by: npx stikfix init`,
247
256
  ``,
248
- `exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`
257
+ useExe ? `exec "${hostExe}" serve --root "${rootArg}"${portArg}` : `exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`
249
258
  ].join("\n");
250
259
  const commandPath = paths.launcher;
251
260
  (0, import_node_fs.writeFileSync)(commandPath, commandContent, { encoding: "utf8", mode: 493 });
@@ -256,7 +265,7 @@ function createLauncherFiles(opts) {
256
265
  `# Stikfix HTTP host launcher`,
257
266
  `# Generated by: npx stikfix init`,
258
267
  ``,
259
- `exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`
268
+ useExe ? `exec "${hostExe}" serve --root "${rootArg}"${portArg}` : `exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`
260
269
  ].join("\n");
261
270
  const shPath = paths.launcher;
262
271
  (0, import_node_fs.writeFileSync)(shPath, shContent, { encoding: "utf8" });
@@ -321,8 +330,8 @@ function registerNativeHost(opts) {
321
330
  const home = opts.home ?? (0, import_node_os.homedir)();
322
331
  const browser = opts.browser ?? "chrome";
323
332
  const manifestPath = nativeManifestPath(plat, home, browser);
324
- const wrapperPath = writeNativeWrapper(opts.hostBinPath, plat, home, browser);
325
- const manifest = buildManifest(opts.extensionId, wrapperPath, browser);
333
+ const manifestTarget = opts.directPath ? (0, import_node_path2.resolve)(opts.hostBinPath) : writeNativeWrapper(opts.hostBinPath, plat, home, browser);
334
+ const manifest = buildManifest(opts.extensionId, manifestTarget, browser);
326
335
  writeManifest(manifest, manifestPath);
327
336
  if (plat === "win32") {
328
337
  const execReg = opts.execReg ?? ((args) => {
@@ -373,6 +382,34 @@ function unregisterNativeHost(opts = {}) {
373
382
  }
374
383
  }
375
384
  }
385
+ function teardownHost(opts = {}) {
386
+ const plat = opts.plat ?? process.platform;
387
+ const home = opts.home ?? (0, import_node_os.homedir)();
388
+ const browser = opts.browser ?? "chrome";
389
+ const result = {
390
+ manifestRemoved: true,
391
+ configPath: CONFIG_PATH(home),
392
+ configRemoved: false
393
+ };
394
+ try {
395
+ unregisterNativeHost({ plat, home, browser });
396
+ } catch (err) {
397
+ result.manifestRemoved = false;
398
+ result.manifestError = err instanceof Error ? err.message : String(err);
399
+ }
400
+ try {
401
+ unregisterStartup({ plat, home });
402
+ } catch (err) {
403
+ result.startupError = err instanceof Error ? err.message : String(err);
404
+ }
405
+ try {
406
+ (0, import_node_fs.rmSync)(result.configPath, { force: true });
407
+ result.configRemoved = true;
408
+ } catch {
409
+ result.configRemoved = false;
410
+ }
411
+ return result;
412
+ }
376
413
 
377
414
  // host/src/extension-id.ts
378
415
  var import_node_crypto2 = require("node:crypto");
@@ -430,8 +467,8 @@ function resolveStartupChoice(forceOn, forceOff, isTTY) {
430
467
  }
431
468
  return false;
432
469
  }
433
- var CONFIG_DIR = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".config", "stikfix");
434
- var CONFIG_PATH = (0, import_node_path3.join)(CONFIG_DIR, "config.json");
470
+ var CONFIG_DIR2 = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".config", "stikfix");
471
+ var CONFIG_PATH2 = (0, import_node_path3.join)(CONFIG_DIR2, "config.json");
435
472
  if (subcommand === "init") {
436
473
  const rawRoot = values["root"];
437
474
  const rawExtId = values["extension-id"];
@@ -449,9 +486,9 @@ if (subcommand === "init") {
449
486
  const name = (0, import_node_path3.basename)(root);
450
487
  const notesDir = (0, import_node_path3.join)(root, "notes");
451
488
  const hostEntryPath = (0, import_node_path3.resolve)((0, import_node_path3.join)(__dirname, "src", "index.js"));
452
- (0, import_node_fs2.mkdirSync)(CONFIG_DIR, { recursive: true });
489
+ (0, import_node_fs2.mkdirSync)(CONFIG_DIR2, { recursive: true });
453
490
  const config = { root, name, notesDir, hostEntry: hostEntryPath, nodePath: process.execPath };
454
- (0, import_node_fs2.writeFileSync)(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf8", mode: 384 });
491
+ (0, import_node_fs2.writeFileSync)(CONFIG_PATH2, JSON.stringify(config, null, 2), { encoding: "utf8", mode: 384 });
455
492
  const hostBinPath = (0, import_node_path3.resolve)((0, import_node_path3.join)(__dirname, "stikfix-native.cjs"));
456
493
  try {
457
494
  registerNativeHost({ extensionId, hostBinPath, browser });
@@ -540,9 +577,9 @@ if (subcommand === "init") {
540
577
  console.log("");
541
578
  console.log(" 2. Start the backend \u2014 double-click the desktop launcher:");
542
579
  if (process.platform === "win32") {
543
- const lnkPath = (0, import_node_path3.join)((0, import_node_os2.homedir)(), "Desktop", "Stikfix Host.lnk");
580
+ const lnkPath = launcherResult.written.find((p) => p.endsWith(".lnk")) ?? "";
544
581
  const batchPath = launcherResult.written.find((p) => p.endsWith(".bat")) ?? "";
545
- if ((0, import_node_fs2.existsSync)(lnkPath)) {
582
+ if (lnkPath && (0, import_node_fs2.existsSync)(lnkPath)) {
546
583
  console.log(' Desktop shortcut: "Stikfix Host" (icon on your Desktop)');
547
584
  } else if (batchPath) {
548
585
  console.log(" Batch file: " + batchPath);
@@ -567,17 +604,13 @@ if (subcommand === "init") {
567
604
  console.log(" npx --yes stikfix@latest init --root " + root);
568
605
  } else if (subcommand === "uninstall") {
569
606
  const browser = resolveBrowser(values["browser"]);
570
- try {
571
- unregisterNativeHost({ browser });
572
- } catch (err) {
573
- console.error("stikfix uninstall: error removing native-host manifest:", String(err));
607
+ const teardown = teardownHost({ browser });
608
+ if (teardown.manifestError) {
609
+ console.error("stikfix uninstall: error removing native-host manifest:", teardown.manifestError);
574
610
  }
575
- try {
576
- unregisterStartup();
577
- } catch (err) {
578
- console.error("stikfix uninstall: error removing startup entry:", String(err));
611
+ if (teardown.startupError) {
612
+ console.error("stikfix uninstall: error removing startup entry:", teardown.startupError);
579
613
  }
580
- (0, import_node_fs2.rmSync)(CONFIG_PATH, { force: true });
581
614
  console.log("stikfix: native host unregistered.");
582
615
  console.log(" manifest removed");
583
616
  console.log(" launcher files removed");
@@ -23,6 +23,7 @@ __export(native_host_exports, {
23
23
  handlePickFolder: () => handlePickFolder,
24
24
  handleStartHost: () => handleStartHost,
25
25
  main: () => main,
26
+ runNativeHost: () => runNativeHost,
26
27
  validateChosenFolder: () => validateChosenFolder
27
28
  });
28
29
  module.exports = __toCommonJS(native_host_exports);
@@ -212,8 +213,11 @@ function handleStartHost(cfg, root, spawnFn = import_node_child_process2.spawn,
212
213
  }
213
214
  const nodePath = typeof cfg.nodePath === "string" && cfg.nodePath.length > 0 ? cfg.nodePath : process.execPath;
214
215
  const hostEntry = resolveHostEntry(cfg);
216
+ const hostExe = typeof cfg.hostExe === "string" && cfg.hostExe.length > 0 ? cfg.hostExe : void 0;
217
+ const spawnCmd = hostExe ?? nodePath;
218
+ const spawnArgs = hostExe ? ["serve", "--root", resolvedRoot] : [hostEntry, "--root", resolvedRoot];
215
219
  try {
216
- const child = spawnFn(nodePath, [hostEntry, "--root", resolvedRoot], {
220
+ const child = spawnFn(spawnCmd, spawnArgs, {
217
221
  detached: true,
218
222
  stdio: "ignore",
219
223
  windowsHide: true
@@ -315,7 +319,17 @@ function main() {
315
319
  process.exit(0);
316
320
  });
317
321
  }
318
- if (typeof require !== "undefined" && typeof module !== "undefined" && require.main === module) {
322
+ async function runNativeHost() {
323
+ main();
324
+ }
325
+ function bundledInExe() {
326
+ try {
327
+ return typeof __STIKFIX_BUNDLED__ !== "undefined" && __STIKFIX_BUNDLED__ === true;
328
+ } catch {
329
+ return false;
330
+ }
331
+ }
332
+ if (!bundledInExe() && typeof require !== "undefined" && typeof module !== "undefined" && require.main === module) {
319
333
  main();
320
334
  }
321
335
  // Annotate the CommonJS export names for ESM import in node:
@@ -323,5 +337,6 @@ if (typeof require !== "undefined" && typeof module !== "undefined" && require.m
323
337
  handlePickFolder,
324
338
  handleStartHost,
325
339
  main,
340
+ runNativeHost,
326
341
  validateChosenFolder
327
342
  });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * URL-path matching and pin position computation for stikfix persistent pins.
3
+ *
4
+ * matchesUrlPath + computePinPosition — pure, node:test-safe (no DOM/chrome at module level).
5
+ * Scroll offsets and anchor rects are PASSED AS PARAMETERS by the caller (pin.ts in Plan 04),
6
+ * which supplies window.scrollX/scrollY and el.getBoundingClientRect() at render time.
7
+ *
8
+ * INVARIANT: No top-level browser API access — no window/document/chrome references.
9
+ * This module imports cleanly under node:test.
10
+ */
11
+ /**
12
+ * Compare two URLs by pathname only, ignoring query string and fragment (D-02).
13
+ * Returns false (not throws) on any malformed URL input.
14
+ *
15
+ * @param noteUrl The URL stored in the note's YAML frontmatter
16
+ * @param pageUrl The current page URL from chrome.tabs.get
17
+ */
18
+ export function matchesUrlPath(noteUrl, pageUrl) {
19
+ try {
20
+ const notePath = new URL(noteUrl).pathname;
21
+ const pagePath = new URL(pageUrl).pathname;
22
+ return notePath === pagePath;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ /**
29
+ * Compute the CSS viewport position for an on-page pin — pure, DOM-free.
30
+ *
31
+ * Branch logic:
32
+ * - orphaned || anchorRect === null: orphaned-fallback at last-known page-absolute
33
+ * rect minus scroll offsets → { left: storedRect.x - scrollX, top: storedRect.y - scrollY, orphaned: true }
34
+ * - !orphaned && anchorRect !== null: use anchorRect x/y directly (already in
35
+ * fixed/viewport coords from getBoundingClientRect or stored viewport coords
36
+ * for free notes) → { left: anchorRect.x, top: anchorRect.y, orphaned: false }
37
+ *
38
+ * The caller (pin.ts) passes:
39
+ * - anchorRect: el.getBoundingClientRect() for element pins, stored viewport
40
+ * coords {x,y,width:0,height:0} for free pins, or null when selector misses.
41
+ * - storedRect: page-absolute rect from frontmatter (for orphaned-fallback).
42
+ * - scrollX/scrollY: window.scrollX / window.scrollY at call time.
43
+ * - orphaned: true when the selector re-query returned null.
44
+ *
45
+ * NEVER reads window.scrollX or window.scrollY internally — caller supplies them.
46
+ *
47
+ * @param anchorRect Live fixed-coord rect from getBoundingClientRect, or null
48
+ * @param storedRect Page-absolute rect from frontmatter (orphaned fallback), or null
49
+ * @param scrollX window.scrollX at call time (passed by caller)
50
+ * @param scrollY window.scrollY at call time (passed by caller)
51
+ * @param orphaned true when selector matched nothing on the current page
52
+ */
53
+ export function computePinPosition(anchorRect, storedRect, scrollX, scrollY, orphaned) {
54
+ if (orphaned || anchorRect === null) {
55
+ // Orphaned fallback: last-known page-absolute rect → convert to viewport via scroll
56
+ return {
57
+ left: (storedRect?.x ?? 0) - scrollX,
58
+ top: (storedRect?.y ?? 0) - scrollY,
59
+ orphaned: true,
60
+ };
61
+ }
62
+ // Element-anchored (live getBoundingClientRect in fixed/viewport coords)
63
+ // OR free-floating (stored viewport coords already in anchorRect x/y)
64
+ return {
65
+ left: anchorRect.x,
66
+ top: anchorRect.y,
67
+ orphaned: false,
68
+ };
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stikfix",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Pin sticky notes on any page — your AI reads them.",
5
5
  "license": "MIT",
6
6
  "author": "Omer Nesher <omernesher@gmail.com>",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "files": [
34
34
  "dist/host/src",
35
+ "dist/lib/pin-position.js",
35
36
  "dist/host/stikfix-init.cjs",
36
37
  "dist/host/stikfix-native.cjs",
37
38
  "public/icon",
@@ -50,13 +51,19 @@
50
51
  "test:lib": "tsc -p tsconfig.lib.json && node --test \"dist/lib/lib/test/**/*.test.js\"",
51
52
  "test": "tsc -p tsconfig.host.json && node --test \"dist/host/test/**/*.test.js\"",
52
53
  "check": "tsc --noEmit && tsc --noEmit -p tsconfig.host.json && node scripts/clean-room-check.mjs && node scripts/host-smoke-test.mjs && npm run test:lib && npm test",
54
+ "pack:crx": "node scripts/pack-crx.mjs",
55
+ "gen:update-xml": "node scripts/gen-update-xml.mjs",
56
+ "build:sea": "node scripts/build-sea.mjs",
57
+ "build:installer": "node scripts/build-installer.mjs",
53
58
  "postinstall": "node -e \"try{require.resolve('wxt');require('node:child_process').execSync('wxt prepare',{stdio:'inherit'})}catch(e){}\""
54
59
  },
55
60
  "devDependencies": {
56
61
  "@medv/finder": "4.0.2",
57
62
  "@types/chrome": "0.1.42",
58
63
  "@types/node": "25.9.1",
64
+ "crx3": "2.0.0",
59
65
  "interactjs": "1.10.27",
66
+ "postject": "1.0.0-alpha.6",
60
67
  "typescript": "6.0.3",
61
68
  "wxt": "0.20.26"
62
69
  },