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 +27 -7
- package/dist/host/src/bootstrap/register.js +108 -17
- package/dist/host/src/config.js +23 -7
- package/dist/host/src/doctor.js +459 -0
- package/dist/host/src/exe-main.js +254 -0
- package/dist/host/src/index.js +156 -118
- package/dist/host/src/native-host.js +34 -7
- package/dist/host/stikfix-init.cjs +61 -28
- package/dist/host/stikfix-native.cjs +17 -2
- package/dist/lib/pin-position.js +69 -0
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](./LICENSE)
|
|
10
10
|
[](https://www.npmjs.com/package/stikfix)
|
|
11
|
+
[](https://github.com/omernesh/stikfix/releases/latest)
|
|
11
12
|
[](#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
|
-
###
|
|
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
|
-
|
|
71
|
+
#### 2. Load the extension
|
|
52
72
|
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
334
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
483
|
-
// point the manifest at a per-OS wrapper that
|
|
484
|
-
|
|
485
|
-
|
|
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 };
|
package/dist/host/src/config.js
CHANGED
|
@@ -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
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|