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.
package/README.md CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
10
10
  [![npm](https://img.shields.io/npm/v/stikfix.svg)](https://www.npmjs.com/package/stikfix)
11
+ [![GitHub Release](https://img.shields.io/github/v/release/omernesh/stikfix?label=Windows%20installer)](https://github.com/omernesh/stikfix/releases/latest)
11
12
  [![Chrome MV3](https://img.shields.io/badge/Chrome-MV3-blue.svg)](#load-the-extension)
12
13
 
13
14
  </div>
@@ -33,7 +34,26 @@ No cloud. No accounts. No sign-up. Everything stays on `127.0.0.1` — **your co
33
34
 
34
35
  ## Quick start
35
36
 
36
- ### 1. Install the host (one command)
37
+ ### Windows: one-click installer (recommended)
38
+
39
+ 1. Download **`stikfix-setup-1.6.0.exe`** from the [latest release](https://github.com/omernesh/stikfix/releases/latest).
40
+ 2. Run it. It needs **administrator rights** — it writes a browser policy so the extension can install itself.
41
+ 3. Pick a setup type:
42
+ - **Complete** — installs the host, force-installs the extension into every Chromium browser it finds (Chrome/Edge/Brave), sets the host to run on Windows login, and adds a desktop shortcut. Fully automatic — no terminal.
43
+ - **Custom** — pick components (the host; which browsers get the extension) and tasks (run-on-startup, desktop shortcut) individually, and choose your notes folder.
44
+ 4. The wizard finishes with a **post-install health check** that verifies the host binds, native messaging is registered, the browser force-install policy is set, and the notes folder is writable.
45
+ 5. Open Chrome/Edge/Brave — the extension is already there, next time the browser starts. You'll see **"Installed by your organization"** next to it; that's expected and correct for a self-hosted extension. It's force-installed via enterprise policy (`ExtensionInstallForcelist`) pointed at a self-hosted update manifest, not a Chrome Web Store listing.
46
+ 6. Open your app, click **Enter Review Mode**, and start dropping notes.
47
+
48
+ The installer ships **`stikfix-host.exe`**, a self-contained binary — no Node.js needs to be installed on the machine. Re-run the health check anytime with `stikfix-host.exe doctor`, or the **"Stikfix Health Check"** shortcut it adds to the Start menu. To remove everything — host, browser policy entry, startup entry, native-messaging registration — use Windows **Add or remove programs**.
49
+
50
+ For advanced use or troubleshooting, the same binary takes subcommands directly: `stikfix-host.exe serve --root <dir>`, `stikfix-host.exe doctor` (prints the health checklist), `stikfix-host.exe register` / `stikfix-host.exe uninstall`.
51
+
52
+ ### Developer path (macOS, Linux, or from source on Windows)
53
+
54
+ Everything the Windows installer automates above, you can also do by hand — this is the path for macOS/Linux, or for building and loading the extension from source.
55
+
56
+ #### 1. Install the host (one command)
37
57
 
38
58
  ```bash
39
59
  npx stikfix init --root /path/to/your/project
@@ -48,9 +68,9 @@ This turnkey installer:
48
68
 
49
69
  > **Keep it current:** re-run `npx --yes stikfix@latest init --root /path/to/your/project` anytime to update the host.
50
70
 
51
- ### 2. Load the extension
71
+ #### 2. Load the extension
52
72
 
53
- For v1.0 the extension is loaded **unpacked** (a Chrome Web Store listing is coming next):
73
+ On the developer path the extension is loaded **unpacked** (a Chrome Web Store listing is coming next — the Windows installer above already force-installs it automatically, so unpacked loading isn't needed there):
54
74
 
55
75
  1. Open `chrome://extensions` (or `edge://extensions`).
56
76
  2. Turn on **Developer mode**.
@@ -58,15 +78,15 @@ For v1.0 the extension is loaded **unpacked** (a Chrome Web Store listing is com
58
78
 
59
79
  The loaded ID matches what the installer printed — nothing to type. (Building `.output/chrome-mv3/` from source is covered under [Development](#development).)
60
80
 
61
- ### 3. Start the backend
81
+ #### 3. Start the backend
62
82
 
63
83
  Double-click the **Stikfix Host** launcher on your Desktop. That's it — no commands, and double-clicking again is safe (it won't launch a second copy). The host listens on a port in the `39240–39260` range; the extension discovers it automatically.
64
84
 
65
- ### 4. Pair in one click
85
+ #### 4. Pair in one click
66
86
 
67
87
  Open the stikfix popup in your toolbar and hit **Pair with host**. The token is handed over automatically through the OS native-messaging channel — you never see it, never paste it. Pair once and it stays paired, even after restarts.
68
88
 
69
- ### 5. Drop a note
89
+ #### 5. Drop a note
70
90
 
71
91
  Open your app, click **Enter Review Mode**, and start dropping notes. The first note on a new site opens an **OS folder picker** so you can choose where that origin's notes live; it's remembered after that. When you're ready, tell your AI agent **"read my notes."**
72
92
 
@@ -80,7 +100,7 @@ Open your app, click **Enter Review Mode**, and start dropping notes. The first
80
100
  - **Notes panel** — a chip-toggled list of every note: counts by status, filter chips, text search, and click-to-jump that scrolls right to the pin. Flip **All pages** to browse every note across the project, not just the current page.
81
101
  - **Live updates** — while Review Mode is on, pins and the panel refresh on their own (~4s, only when the tab is visible) as the agent writes replies and resolves notes. No manual reload.
82
102
  - **Per-origin project routing** — the first note on a new site opens an OS folder picker; after that, every tab routes to the right project's `notes/` folder automatically. No per-note picking.
83
- - **Cross-browser host** — one `npx stikfix init` registers Chrome and Edge in a single pass.
103
+ - **Cross-browser host** — one `npx stikfix init` registers Chrome and Edge in a single pass. On Windows, the one-click installer does this (and the browser-side install) for you.
84
104
  - **The `review-notes` AI skill** — the portable agent half of the loop (see below).
85
105
 
86
106
  ## How notes reach your AI
@@ -268,15 +268,22 @@ export function createLauncherFiles(opts) {
268
268
  const hostEntry = opts.hostEntryPath;
269
269
  const rootArg = opts.root;
270
270
  const portArg = opts.port !== undefined ? ` --port ${opts.port}` : '';
271
+ // Standalone-exe launcher: run `"<exe>" serve --root ...` instead of
272
+ // `"node" "<hostEntry>" --root ...`. Absent → exact prior node behavior.
273
+ const useExe = typeof opts.hostExe === 'string' && opts.hostExe.length > 0;
274
+ const hostExe = opts.hostExe ?? '';
271
275
  if (plat === 'win32') {
272
276
  // Write batch file — plain text, no exec required
277
+ const batchLaunch = useExe
278
+ ? `"${hostExe}" serve --root "${rootArg}"${portArg}`
279
+ : `"${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`;
273
280
  const batchContent = [
274
281
  '@echo off',
275
282
  `rem Stikfix HTTP host launcher — double-click to start the backend`,
276
283
  `rem Generated by: npx stikfix init`,
277
284
  `rem Root: ${rootArg}`,
278
285
  ``,
279
- `"${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`,
286
+ batchLaunch,
280
287
  `if %ERRORLEVEL% NEQ 0 pause`,
281
288
  ].join('\r\n');
282
289
  const batchPath = paths.launcher;
@@ -287,17 +294,25 @@ export function createLauncherFiles(opts) {
287
294
  // VBS string-literal escaping: a literal " is doubled ("").
288
295
  const vq = (s) => s.replace(/"/g, '""');
289
296
  const vbsHostEntry = vq(hostEntry);
297
+ const vbsHostExe = vq(hostExe);
290
298
  const vbsRoot = vq(rootArg);
291
299
  const vbsPortArg = opts.port !== undefined ? ` --port ${opts.port}` : '';
300
+ // The hidden-launch line: exe mode runs `"<exe>" serve --root ...`, node
301
+ // mode runs `node "<hostEntry>" --root ...`. Both go through `cmd /c` so a
302
+ // relative `node` on PATH resolves and the window stays hidden (style 0).
303
+ const vbsRunLine = useExe
304
+ ? `sh.Run "cmd /c """ & hostExe & """ serve --root """ & root & """${vbsPortArg}", 0, False`
305
+ : `sh.Run "cmd /c node """ & hostEntry & """ --root """ & root & """${vbsPortArg}", 0, False`;
292
306
  const vbsContent = [
293
307
  'Option Explicit',
294
- 'Dim sh, fso, hostEntry, root, portFile, msg, port, f',
308
+ 'Dim sh, fso, hostEntry, hostExe, root, portFile, msg, port, f',
295
309
  'Set sh = CreateObject("WScript.Shell")',
296
310
  'Set fso = CreateObject("Scripting.FileSystemObject")',
297
311
  `hostEntry = "${vbsHostEntry}"`,
312
+ `hostExe = "${vbsHostExe}"`,
298
313
  `root = "${vbsRoot}"`,
299
314
  "' Launch the host hidden (window style 0), do not wait",
300
- `sh.Run "cmd /c node """ & hostEntry & """ --root """ & root & """${vbsPortArg}", 0, False`,
315
+ vbsRunLine,
301
316
  "' Give it a moment to bind and write the port file",
302
317
  'WScript.Sleep 1800',
303
318
  'msg = "Stikfix host is running." & vbCrLf & vbCrLf & "You can start dropping notes."',
@@ -319,34 +334,56 @@ export function createLauncherFiles(opts) {
319
334
  // execFile (not exec) — no shell spawned, no injection vector.
320
335
  // If this fails for any reason, fall through to the warning (batch file is the fallback).
321
336
  if (paths.shortcut) {
322
- const lnkPath = paths.shortcut;
337
+ // paths.shortcut (from getLauncherPaths) is a HARDCODED GUESS —
338
+ // join(home, 'Desktop', ...) — which is wrong when Windows OneDrive
339
+ // "Known Folder Move" (or a custom Shell-Folder registry override)
340
+ // redirects the Desktop elsewhere. Resolve the REAL current Desktop
341
+ // folder at shortcut-creation time via PowerShell's
342
+ // [Environment]::GetFolderPath('Desktop'), which honors that
343
+ // redirection, and build the .lnk path from that instead. The guess is
344
+ // kept only as the non-fatal fallback message below.
345
+ const guessedLnkPath = paths.shortcut;
323
346
  const iconPath = opts.iconPath ?? '';
324
347
  // Build PowerShell script as a single string — paths passed as PS variables
325
348
  // set from the -Command string. No shell metacharacter interpolation from
326
349
  // external data; all values are constants determined at registration time.
327
350
  // PowerShell single-quote escaping: replace ' with '' in path strings.
328
- const safeLnkPath = lnkPath.replace(/'/g, "''");
351
+ const safeLnkFileName = LAUNCHER_LNK_FILENAME.replace(/'/g, "''");
329
352
  const safeIconPath = iconPath.replace(/'/g, "''");
330
353
  const wscriptPath = join(process.env['SystemRoot'] ?? 'C:\\Windows', 'System32', 'wscript.exe');
331
354
  const safeWscript = wscriptPath.replace(/'/g, "''");
332
355
  const safeVbsPath = vbsPath.replace(/'/g, "''");
333
- const psScript = `$ws = New-Object -ComObject WScript.Shell;` +
334
- `$s = $ws.CreateShortcut('${safeLnkPath}');` +
356
+ // $desktop resolves the REAL Desktop (OneDrive KFM / Shell-Folder aware);
357
+ // the script prints the final .lnk path on stdout so Node can capture the
358
+ // ACTUAL path created instead of trusting the hardcoded guess.
359
+ const psScript = `$desktop = [Environment]::GetFolderPath('Desktop');` +
360
+ `$lnkPath = Join-Path $desktop '${safeLnkFileName}';` +
361
+ `$ws = New-Object -ComObject WScript.Shell;` +
362
+ `$s = $ws.CreateShortcut($lnkPath);` +
335
363
  `$s.TargetPath = '${safeWscript}';` +
336
364
  `$s.Arguments = '"${safeVbsPath}"';` +
337
365
  `$s.Description = 'Start the Stikfix HTTP backend host';` +
338
366
  (safeIconPath ? `$s.IconLocation = '${safeIconPath},0';` : '') +
339
- `$s.Save()`;
367
+ `$s.Save();` +
368
+ `Write-Output $lnkPath`;
340
369
  // execFileSync — NEVER exec; arg array is static with respect to user input.
341
370
  // Synchronous so init reports the shortcut accurately (the async variant
342
371
  // raced existsSync in the CLI and printed a false "skipped" message).
343
372
  try {
344
- execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', psScript], { timeout: 15_000, stdio: 'ignore' });
373
+ const stdout = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', psScript], { timeout: 15_000, encoding: 'utf8' });
374
+ // The resolved .lnk path is the last non-empty stdout line. Fall back to
375
+ // the hardcoded guess only if PowerShell somehow printed nothing.
376
+ const resolvedLnkPath = stdout
377
+ .split(/\r?\n/)
378
+ .map((line) => line.trim())
379
+ .filter(Boolean)
380
+ .pop() ?? guessedLnkPath;
345
381
  // Only record the shortcut once it actually exists on disk.
346
- written.push(lnkPath);
382
+ written.push(resolvedLnkPath);
347
383
  }
348
384
  catch (err) {
349
- // Non-fatal: batch file is the acceptable fallback
385
+ // Non-fatal: batch file is the acceptable fallback. The shortcut was not
386
+ // created at guessedLnkPath (or anywhere else), matching prior behavior.
350
387
  warnings.push(`Desktop shortcut creation failed (non-fatal): ${String(err.message)}. ` +
351
388
  `The batch file at ${batchPath} can be used directly.`);
352
389
  }
@@ -359,7 +396,9 @@ export function createLauncherFiles(opts) {
359
396
  `# Stikfix HTTP host launcher — double-click in Finder or drag to Dock`,
360
397
  `# Generated by: npx stikfix init`,
361
398
  ``,
362
- `exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`,
399
+ useExe
400
+ ? `exec "${hostExe}" serve --root "${rootArg}"${portArg}`
401
+ : `exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`,
363
402
  ].join('\n');
364
403
  const commandPath = paths.launcher;
365
404
  writeFileSync(commandPath, commandContent, { encoding: 'utf8', mode: 0o755 });
@@ -372,7 +411,9 @@ export function createLauncherFiles(opts) {
372
411
  `# Stikfix HTTP host launcher`,
373
412
  `# Generated by: npx stikfix init`,
374
413
  ``,
375
- `exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`,
414
+ useExe
415
+ ? `exec "${hostExe}" serve --root "${rootArg}"${portArg}`
416
+ : `exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`,
376
417
  ].join('\n');
377
418
  const shPath = paths.launcher;
378
419
  writeFileSync(shPath, shContent, { encoding: 'utf8' });
@@ -479,10 +520,14 @@ export function registerNativeHost(opts) {
479
520
  const home = opts.home ?? homedir();
480
521
  const browser = opts.browser ?? 'chrome';
481
522
  const manifestPath = nativeManifestPath(plat, home, browser);
482
- // Chrome's CreateProcess (and Firefox on Windows) cannot run a .cjs directly —
483
- // point the manifest at a per-OS wrapper that runs `node <abs cjs>` instead.
484
- const wrapperPath = writeNativeWrapper(opts.hostBinPath, plat, home, browser);
485
- const manifest = buildManifest(opts.extensionId, wrapperPath, browser);
523
+ // Default (npx/node install): Chrome's CreateProcess (and Firefox on Windows)
524
+ // cannot run a .cjs directly — point the manifest at a per-OS wrapper that
525
+ // runs `node <abs cjs>`. Standalone-exe install (directPath): the exe self-
526
+ // detects native mode, so point the manifest straight at it — no wrapper.
527
+ const manifestTarget = opts.directPath
528
+ ? resolve(opts.hostBinPath)
529
+ : writeNativeWrapper(opts.hostBinPath, plat, home, browser);
530
+ const manifest = buildManifest(opts.extensionId, manifestTarget, browser);
486
531
  writeManifest(manifest, manifestPath);
487
532
  if (plat === 'win32') {
488
533
  const execReg = opts.execReg ?? ((args) => {
@@ -613,5 +658,51 @@ export function enumerateArtifacts(opts = {}) {
613
658
  : [];
614
659
  return { paths, registryKeys };
615
660
  }
661
+ /**
662
+ * Shared uninstall teardown used by BOTH `bin/stikfix.ts uninstall` and the
663
+ * standalone exe's `stikfix-host uninstall` subcommand, so the two entry
664
+ * points stay behaviorally equivalent and neither duplicates the removal
665
+ * logic inline:
666
+ * - remove the native-messaging manifest + HKCU registry keys, launcher
667
+ * files, and Desktop shortcut (via unregisterNativeHost)
668
+ * - remove the Windows startup (HKCU Run) autoload entry (via unregisterStartup)
669
+ * - remove the stikfix config.json
670
+ *
671
+ * Idempotent: every step tolerates an already-absent file/key — matches the
672
+ * existing idempotent contracts of unregisterNativeHost/unregisterStartup.
673
+ * No console output here — callers own their own user-facing messages/format
674
+ * (bin/stikfix.ts prints plain text lines; the exe prints JSON).
675
+ */
676
+ export function teardownHost(opts = {}) {
677
+ const plat = opts.plat ?? process.platform;
678
+ const home = opts.home ?? homedir();
679
+ const browser = opts.browser ?? 'chrome';
680
+ const result = {
681
+ manifestRemoved: true,
682
+ configPath: CONFIG_PATH(home),
683
+ configRemoved: false,
684
+ };
685
+ try {
686
+ unregisterNativeHost({ plat, home, browser });
687
+ }
688
+ catch (err) {
689
+ result.manifestRemoved = false;
690
+ result.manifestError = err instanceof Error ? err.message : String(err);
691
+ }
692
+ try {
693
+ unregisterStartup({ plat, home });
694
+ }
695
+ catch (err) {
696
+ result.startupError = err instanceof Error ? err.message : String(err);
697
+ }
698
+ try {
699
+ rmSync(result.configPath, { force: true });
700
+ result.configRemoved = true;
701
+ }
702
+ catch {
703
+ result.configRemoved = false;
704
+ }
705
+ return result;
706
+ }
616
707
  // Re-export isInsideDir for consumers that need path validation
617
708
  export { isInsideDir };
@@ -13,13 +13,29 @@ import { resolve, join, basename, dirname } from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
14
14
  import { randomUUID } from 'node:crypto';
15
15
  import { isInsideDir } from './security.js';
16
- // ---------------------------------------------------------------------------
17
- // VERSION read from package.json at runtime (Pattern 11)
18
- // dist/host/src/config.js ../../../package.json
19
- // ---------------------------------------------------------------------------
20
- const __dirname = dirname(fileURLToPath(import.meta.url));
21
- const _pkg = JSON.parse(readFileSync(join(__dirname, '../../../package.json'), 'utf8'));
22
- export const VERSION = _pkg.version;
16
+ function resolveVersion() {
17
+ // 1. SEA exe: the bundle has no package.json at a stable relative path, so the
18
+ // version is inlined at build time. `typeof` guards against ReferenceError
19
+ // in builds where the constant was never defined.
20
+ try {
21
+ if (typeof __STIKFIX_VERSION__ === 'string' && __STIKFIX_VERSION__.length > 0) {
22
+ return __STIKFIX_VERSION__;
23
+ }
24
+ }
25
+ catch {
26
+ // Not defined in this build — fall through to the on-disk read.
27
+ }
28
+ // 2. Normal tsc/node build: dist/host/src/config.js → ../../../package.json.
29
+ try {
30
+ const dir = dirname(fileURLToPath(import.meta.url));
31
+ const pkg = JSON.parse(readFileSync(join(dir, '../../../package.json'), 'utf8'));
32
+ return pkg.version;
33
+ }
34
+ catch {
35
+ return '0.0.0';
36
+ }
37
+ }
38
+ export const VERSION = resolveVersion();
23
39
  // ---------------------------------------------------------------------------
24
40
  // resolveConfigValues — three-tier env resolution (exported for testing)
25
41
  // ---------------------------------------------------------------------------