@jobshimo/browser-link 0.7.15 → 0.8.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.
Files changed (45) hide show
  1. package/dist/agent-instructions/claude.d.ts +2 -0
  2. package/dist/agent-instructions/claude.js +29 -0
  3. package/dist/agent-instructions/claude.js.map +1 -0
  4. package/dist/agent-instructions/content.d.ts +47 -0
  5. package/dist/agent-instructions/content.js +119 -0
  6. package/dist/agent-instructions/content.js.map +1 -0
  7. package/dist/agent-instructions/copilot.d.ts +2 -0
  8. package/dist/agent-instructions/copilot.js +31 -0
  9. package/dist/agent-instructions/copilot.js.map +1 -0
  10. package/dist/agent-instructions/errors.d.ts +25 -0
  11. package/dist/agent-instructions/errors.js +39 -0
  12. package/dist/agent-instructions/errors.js.map +1 -0
  13. package/dist/agent-instructions/file-ops.d.ts +8 -0
  14. package/dist/agent-instructions/file-ops.js +171 -0
  15. package/dist/agent-instructions/file-ops.js.map +1 -0
  16. package/dist/agent-instructions/index.d.ts +9 -0
  17. package/dist/agent-instructions/index.js +20 -0
  18. package/dist/agent-instructions/index.js.map +1 -0
  19. package/dist/agent-instructions/opencode.d.ts +2 -0
  20. package/dist/agent-instructions/opencode.js +30 -0
  21. package/dist/agent-instructions/opencode.js.map +1 -0
  22. package/dist/agent-instructions/types.d.ts +44 -0
  23. package/dist/agent-instructions/types.js +2 -0
  24. package/dist/agent-instructions/types.js.map +1 -0
  25. package/dist/cli.js +63 -0
  26. package/dist/cli.js.map +1 -1
  27. package/dist/commands/doctor.d.ts +5 -0
  28. package/dist/commands/doctor.js +40 -1
  29. package/dist/commands/doctor.js.map +1 -1
  30. package/dist/commands/instructions.d.ts +28 -0
  31. package/dist/commands/instructions.js +110 -0
  32. package/dist/commands/instructions.js.map +1 -0
  33. package/dist/extension/manifest.json +1 -1
  34. package/dist/ui/app.js +5 -1
  35. package/dist/ui/app.js.map +1 -1
  36. package/dist/ui/screens/agent-instructions.d.ts +6 -0
  37. package/dist/ui/screens/agent-instructions.js +108 -0
  38. package/dist/ui/screens/agent-instructions.js.map +1 -0
  39. package/dist/ui/screens/index.d.ts +1 -0
  40. package/dist/ui/screens/index.js +1 -0
  41. package/dist/ui/screens/index.js.map +1 -1
  42. package/dist/ui/screens/menu.d.ts +1 -1
  43. package/dist/ui/screens/menu.js +3 -0
  44. package/dist/ui/screens/menu.js.map +1 -1
  45. package/package.json +2 -1
@@ -0,0 +1,2 @@
1
+ import type { InstructionsInstaller } from './types.js';
2
+ export declare const claudeInstructionsInstaller: InstructionsInstaller;
@@ -0,0 +1,29 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { detectAt, installAt, uninstallAt } from './file-ops.js';
4
+ /**
5
+ * Claude Code reads `~/.claude/CLAUDE.md` as the user-level global
6
+ * instructions file (applied to every project). That is where the block
7
+ * lives so the trigger list reaches every Claude session, regardless of
8
+ * which repo the user is in.
9
+ */
10
+ function file() {
11
+ return join(homedir(), '.claude', 'CLAUDE.md');
12
+ }
13
+ export const claudeInstructionsInstaller = {
14
+ id: 'claude',
15
+ displayName: 'Claude Code',
16
+ filePath() {
17
+ return file();
18
+ },
19
+ detect() {
20
+ return detectAt(file());
21
+ },
22
+ install() {
23
+ return installAt(file(), 'Claude Code');
24
+ },
25
+ uninstall() {
26
+ return uninstallAt(file(), 'Claude Code');
27
+ },
28
+ };
29
+ //# sourceMappingURL=claude.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/agent-instructions/claude.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGjE;;;;;GAKG;AACH,SAAS,IAAI;IACX,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,CAAC,MAAM,2BAA2B,GAA0B;IAChE,EAAE,EAAE,QAAQ;IACZ,WAAW,EAAE,aAAa;IAE1B,QAAQ;QACN,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC;IAED,MAAM;QACJ,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1B,CAAC;IAED,OAAO;QACL,OAAO,SAAS,CAAC,IAAI,EAAE,EAAE,aAAa,CAAC,CAAC;IAC1C,CAAC;IAED,SAAS;QACP,OAAO,WAAW,CAAC,IAAI,EAAE,EAAE,aAAa,CAAC,CAAC;IAC5C,CAAC;CACF,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The block we drop into each agent's global instructions markdown file.
3
+ * Wrapped in HTML-comment marker fences so:
4
+ * - rendered markdown viewers hide the markers,
5
+ * - the installer can detect / re-find / replace the exact span,
6
+ * - other content in the same .md file is left untouched.
7
+ *
8
+ * The version inside the begin marker lets a newer browser-link tell whether
9
+ * an installed block is current; install() always rewrites to the running
10
+ * version, so reinstalling is the upgrade path.
11
+ */
12
+ export declare const BEGIN_PREFIX = "<!-- browser-link:instructions:begin";
13
+ export declare const END_MARKER = "<!-- browser-link:instructions:end -->";
14
+ export declare function beginMarker(version?: string): string;
15
+ /** Regex that matches either an explicit version marker or a legacy one
16
+ * with no version. The named `version` group is set when a version is
17
+ * present (a plain capture group is typed as `string` by TS, which would
18
+ * collapse the `undefined` branch we want; the named-group form keeps
19
+ * `version` as `string | undefined`). */
20
+ export declare const BEGIN_RE: RegExp;
21
+ export declare const END_RE: RegExp;
22
+ /** Full fenced block to write into the file, including a trailing newline.
23
+ * The line between markers carries the managed-by stamp so a user reading
24
+ * the file knows it is auto-generated and how to refresh it.
25
+ *
26
+ * `eol` controls the line separator. Default is LF; pass CRLF to match a
27
+ * Windows-edited file's dominant line ending. The body() helper composes
28
+ * its own lines with `\n`; we replace them when joining so the produced
29
+ * block is uniformly LF or CRLF — never mixed. */
30
+ export declare function block(version?: string, eol?: '\n' | '\r\n'): string;
31
+ /** Count occurrences of the BEGIN marker in `text`. The base BEGIN_RE has
32
+ * no `g` flag (it's used for single-span detection); this helper compiles
33
+ * the same pattern with `g` so duplicates can be diagnosed. */
34
+ export declare function countBeginMarkers(text: string): number;
35
+ /** Pick the dominant line ending of `text`. CRLF wins only when it is the
36
+ * majority of newline sequences; otherwise LF. Used by installAt to keep
37
+ * Windows-edited files from acquiring a mixed-EOL block. */
38
+ export declare function detectEol(text: string): '\n' | '\r\n';
39
+ /** Locate the block boundaries in the given file text. Returns null when
40
+ * either marker is missing. Used by detect/install/uninstall to act on the
41
+ * exact span the installer owns. */
42
+ export interface BlockSpan {
43
+ startIndex: number;
44
+ endIndex: number;
45
+ installedVersion: string | null;
46
+ }
47
+ export declare function findBlockSpan(text: string): BlockSpan | null;
@@ -0,0 +1,119 @@
1
+ import { VERSION } from '../version.js';
2
+ /**
3
+ * The block we drop into each agent's global instructions markdown file.
4
+ * Wrapped in HTML-comment marker fences so:
5
+ * - rendered markdown viewers hide the markers,
6
+ * - the installer can detect / re-find / replace the exact span,
7
+ * - other content in the same .md file is left untouched.
8
+ *
9
+ * The version inside the begin marker lets a newer browser-link tell whether
10
+ * an installed block is current; install() always rewrites to the running
11
+ * version, so reinstalling is the upgrade path.
12
+ */
13
+ export const BEGIN_PREFIX = '<!-- browser-link:instructions:begin';
14
+ export const END_MARKER = '<!-- browser-link:instructions:end -->';
15
+ export function beginMarker(version = VERSION) {
16
+ return `${BEGIN_PREFIX} v${version} -->`;
17
+ }
18
+ /** Regex that matches either an explicit version marker or a legacy one
19
+ * with no version. The named `version` group is set when a version is
20
+ * present (a plain capture group is typed as `string` by TS, which would
21
+ * collapse the `undefined` branch we want; the named-group form keeps
22
+ * `version` as `string | undefined`). */
23
+ export const BEGIN_RE = /<!--\s*browser-link:instructions:begin(?:\s+v(?<version>\d+\.\d+\.\d+))?\s*-->/;
24
+ export const END_RE = /<!--\s*browser-link:instructions:end\s*-->/;
25
+ /** Block body. Plain English by design — every LLM understands it, and a
26
+ * mixed-language user is still served. Triggers-first; no marketing copy. */
27
+ function body() {
28
+ return [
29
+ '## browser-link — when to reach for it',
30
+ '',
31
+ 'When you have access to `browser.*` MCP tools (the bridge exposed by',
32
+ '@jobshimo/browser-link), reach for them BEFORE speculating about UI',
33
+ 'behavior. Tabs the user has not explicitly connected through the',
34
+ 'Chrome extension are invisible to you; never reason about state you',
35
+ 'cannot see.',
36
+ '',
37
+ '### Triggers — call these reflexively',
38
+ '',
39
+ '- **User reports a UI bug, broken layout, "the button does not fire",',
40
+ ' or asks "does X work" in a web app**: call `browser.list_tabs` first.',
41
+ ' If one tab is connected, snapshot it. If several, ask which.',
42
+ '- **Before suggesting a code change to a UI component**: verify the',
43
+ ' current page state with `browser.snapshot`. Do not guess.',
44
+ '- **Any non-trivial debugging session on a web app the user mentioned**:',
45
+ ' call `browser.map.recall` with the tab origin first. Past selectors,',
46
+ ' flows and gotchas come back; you save tokens and avoid re-deriving.',
47
+ '- **Tool call fails with "Tab not connected"**: call `browser.events`',
48
+ ' and look for `tab-renamed` entries before retrying.',
49
+ '- **After any non-trivial flow you discovered end-to-end** (opened a',
50
+ ' dialog, completed a form, found a setting): persist it with',
51
+ ' `browser.map.save`. UI structure only — never save domain data',
52
+ ' (IDs, names, dates).',
53
+ '',
54
+ '### Multi-agent cooperation',
55
+ '',
56
+ 'When several MCP clients share one bridge (multi-agent mode), call',
57
+ '`browser.claim_tab` before operating on a tab so other agents see it',
58
+ 'is in use. Release with `browser.release_tab` when done.',
59
+ ].join('\n');
60
+ }
61
+ /** Full fenced block to write into the file, including a trailing newline.
62
+ * The line between markers carries the managed-by stamp so a user reading
63
+ * the file knows it is auto-generated and how to refresh it.
64
+ *
65
+ * `eol` controls the line separator. Default is LF; pass CRLF to match a
66
+ * Windows-edited file's dominant line ending. The body() helper composes
67
+ * its own lines with `\n`; we replace them when joining so the produced
68
+ * block is uniformly LF or CRLF — never mixed. */
69
+ export function block(version = VERSION, eol = '\n') {
70
+ const parts = [
71
+ beginMarker(version),
72
+ `<!-- managed-by: browser-link v${version}; do not edit between markers; run \`browser-link instructions install\` to refresh. -->`,
73
+ '',
74
+ body(),
75
+ '',
76
+ END_MARKER,
77
+ '',
78
+ ];
79
+ if (eol === '\n')
80
+ return parts.join('\n');
81
+ // body() returned a multi-line string joined with \n; flip each \n to \r\n.
82
+ return parts.map((p) => p.replace(/\n/g, '\r\n')).join('\r\n');
83
+ }
84
+ /** Count occurrences of the BEGIN marker in `text`. The base BEGIN_RE has
85
+ * no `g` flag (it's used for single-span detection); this helper compiles
86
+ * the same pattern with `g` so duplicates can be diagnosed. */
87
+ export function countBeginMarkers(text) {
88
+ return (text.match(/<!--\s*browser-link:instructions:begin(?:\s+v\d+\.\d+\.\d+)?\s*-->/g) ?? [])
89
+ .length;
90
+ }
91
+ /** Pick the dominant line ending of `text`. CRLF wins only when it is the
92
+ * majority of newline sequences; otherwise LF. Used by installAt to keep
93
+ * Windows-edited files from acquiring a mixed-EOL block. */
94
+ export function detectEol(text) {
95
+ const crlf = (text.match(/\r\n/g) ?? []).length;
96
+ const lfTotal = (text.match(/\n/g) ?? []).length;
97
+ const loneLf = lfTotal - crlf;
98
+ return crlf > loneLf && crlf > 0 ? '\r\n' : '\n';
99
+ }
100
+ export function findBlockSpan(text) {
101
+ const begin = BEGIN_RE.exec(text);
102
+ if (!begin)
103
+ return null;
104
+ const afterBegin = begin.index + begin[0].length;
105
+ const end = END_RE.exec(text.slice(afterBegin));
106
+ if (!end)
107
+ return null;
108
+ const endMarkerStartAbsolute = afterBegin + end.index;
109
+ const endMarkerEndAbsolute = endMarkerStartAbsolute + end[0].length;
110
+ // Consume the trailing newline after the end marker, if any.
111
+ const nextChar = text[endMarkerEndAbsolute];
112
+ const endIndex = nextChar === '\n' ? endMarkerEndAbsolute + 1 : endMarkerEndAbsolute;
113
+ return {
114
+ startIndex: begin.index,
115
+ endIndex,
116
+ installedVersion: begin.groups?.version ?? null,
117
+ };
118
+ }
119
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/agent-instructions/content.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { InstructionsInstaller } from './types.js';
2
+ export declare const copilotInstructionsInstaller: InstructionsInstaller;
@@ -0,0 +1,31 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { detectAt, installAt, uninstallAt } from './file-ops.js';
4
+ /**
5
+ * GitHub Copilot CLI keeps its config under `~/.copilot/` (overridable via
6
+ * COPILOT_HOME, same env var the rest of our Copilot integration honours).
7
+ * AGENTS.md alongside `mcp-config.json` is the convention recent versions
8
+ * pick up; older releases ignore the file, in which case the block sits
9
+ * there harmlessly until they catch up.
10
+ */
11
+ function file() {
12
+ const root = process.env.COPILOT_HOME ?? join(homedir(), '.copilot');
13
+ return join(root, 'AGENTS.md');
14
+ }
15
+ export const copilotInstructionsInstaller = {
16
+ id: 'copilot',
17
+ displayName: 'GitHub Copilot CLI',
18
+ filePath() {
19
+ return file();
20
+ },
21
+ detect() {
22
+ return detectAt(file());
23
+ },
24
+ install() {
25
+ return installAt(file(), 'GitHub Copilot CLI');
26
+ },
27
+ uninstall() {
28
+ return uninstallAt(file(), 'GitHub Copilot CLI');
29
+ },
30
+ };
31
+ //# sourceMappingURL=copilot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copilot.js","sourceRoot":"","sources":["../../src/agent-instructions/copilot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGjE;;;;;;GAMG;AACH,SAAS,IAAI;IACX,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;IACrE,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,CAAC,MAAM,4BAA4B,GAA0B;IACjE,EAAE,EAAE,SAAS;IACb,WAAW,EAAE,oBAAoB;IAEjC,QAAQ;QACN,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC;IAED,MAAM;QACJ,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1B,CAAC;IAED,OAAO;QACL,OAAO,SAAS,CAAC,IAAI,EAAE,EAAE,oBAAoB,CAAC,CAAC;IACjD,CAAC;IAED,SAAS;QACP,OAAO,WAAW,CAAC,IAAI,EAAE,EAAE,oBAAoB,CAAC,CAAC;IACnD,CAAC;CACF,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Typed errors thrown by the agent-instructions file operations. Surfaced
3
+ * in the Ink screen as red banners; the CLI prints them straight to stderr.
4
+ * Keeping them in their own module avoids cycles between file-ops and the
5
+ * UI / command layers that catch them.
6
+ */
7
+ /** Thrown when the target file is a symlink. We refuse to write through
8
+ * symlinks because the user's intent is ambiguous: editing the target
9
+ * may not be what they meant when they installed the symlink. The error
10
+ * message embeds a paste-ready block so the user can install it manually
11
+ * in the resolved target. */
12
+ export declare class SymlinkRefusedError extends Error {
13
+ readonly source: string;
14
+ readonly target: string;
15
+ readonly blockContent: string;
16
+ constructor(source: string, target: string, blockContent: string);
17
+ }
18
+ /** Thrown when the target file contains more than one BEGIN marker. The
19
+ * installer cannot know which span to refresh; the user must resolve the
20
+ * duplication by hand before install/uninstall can proceed. */
21
+ export declare class CorruptBlockError extends Error {
22
+ readonly filePath: string;
23
+ readonly reason: 'multiple-begin-markers';
24
+ constructor(filePath: string, reason: 'multiple-begin-markers');
25
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Typed errors thrown by the agent-instructions file operations. Surfaced
3
+ * in the Ink screen as red banners; the CLI prints them straight to stderr.
4
+ * Keeping them in their own module avoids cycles between file-ops and the
5
+ * UI / command layers that catch them.
6
+ */
7
+ /** Thrown when the target file is a symlink. We refuse to write through
8
+ * symlinks because the user's intent is ambiguous: editing the target
9
+ * may not be what they meant when they installed the symlink. The error
10
+ * message embeds a paste-ready block so the user can install it manually
11
+ * in the resolved target. */
12
+ export class SymlinkRefusedError extends Error {
13
+ source;
14
+ target;
15
+ blockContent;
16
+ constructor(source, target, blockContent) {
17
+ super(`${source} is a symlink to ${target}. Refusing to write through it. ` +
18
+ `Paste the block below into ${target} manually:\n\n${blockContent}`);
19
+ this.source = source;
20
+ this.target = target;
21
+ this.blockContent = blockContent;
22
+ this.name = 'SymlinkRefusedError';
23
+ }
24
+ }
25
+ /** Thrown when the target file contains more than one BEGIN marker. The
26
+ * installer cannot know which span to refresh; the user must resolve the
27
+ * duplication by hand before install/uninstall can proceed. */
28
+ export class CorruptBlockError extends Error {
29
+ filePath;
30
+ reason;
31
+ constructor(filePath, reason) {
32
+ super(`${filePath} contains multiple browser-link instruction blocks ` +
33
+ `(${reason}). Refusing to touch it — please resolve the duplication manually.`);
34
+ this.filePath = filePath;
35
+ this.reason = reason;
36
+ this.name = 'CorruptBlockError';
37
+ }
38
+ }
39
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/agent-instructions/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;6BAI6B;AAC7B,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAE1B;IACA;IACA;IAHlB,YACkB,MAAc,EACd,MAAc,EACd,YAAoB;QAEpC,KAAK,CACH,GAAG,MAAM,oBAAoB,MAAM,kCAAkC;YACnE,8BAA8B,MAAM,iBAAiB,YAAY,EAAE,CACtE,CAAC;QAPc,WAAM,GAAN,MAAM,CAAQ;QACd,WAAM,GAAN,MAAM,CAAQ;QACd,iBAAY,GAAZ,YAAY,CAAQ;QAMpC,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED;;+DAE+D;AAC/D,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAExB;IACA;IAFlB,YACkB,QAAgB,EAChB,MAAgC;QAEhD,KAAK,CACH,GAAG,QAAQ,qDAAqD;YAC9D,IAAI,MAAM,oEAAoE,CACjF,CAAC;QANc,aAAQ,GAAR,QAAQ,CAAQ;QAChB,WAAM,GAAN,MAAM,CAA0B;QAMhD,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF"}
@@ -0,0 +1,8 @@
1
+ import type { InstructionsDetect } from './types.js';
2
+ export declare function detectAt(filePath: string): InstructionsDetect;
3
+ /**
4
+ * Insert or refresh the block in `filePath`. Returns a one-line description
5
+ * of what changed, suitable for surfacing to the user.
6
+ */
7
+ export declare function installAt(filePath: string, displayName: string): string;
8
+ export declare function uninstallAt(filePath: string, displayName: string): string;
@@ -0,0 +1,171 @@
1
+ import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, realpathSync, renameSync, unlinkSync, writeSync, } from 'node:fs';
2
+ import { basename, dirname, join } from 'node:path';
3
+ import { VERSION } from '../version.js';
4
+ import { block, countBeginMarkers, detectEol, findBlockSpan } from './content.js';
5
+ import { CorruptBlockError, SymlinkRefusedError } from './errors.js';
6
+ /**
7
+ * Shared file-level operations for the three agent-instructions installers.
8
+ * Per-client modules only know which path their target file lives at; the
9
+ * marker handling and the text splice are the same everywhere.
10
+ */
11
+ /** Atomic write via a sibling temp file + fsync + rename. Closes the
12
+ * TOCTOU window between existsSync()/stat and writeFileSync() that
13
+ * CodeQL flagged as js/file-system-race. Cleans up the temp file if
14
+ * the rename fails (or any earlier step throws). The 0o600 create-mode
15
+ * keeps the temp file private while it lives. */
16
+ function atomicWrite(filePath, content) {
17
+ const dir = dirname(filePath);
18
+ const tmp = join(dir, `.${basename(filePath)}.${process.pid}-${Date.now()}.tmp`);
19
+ let fd = null;
20
+ try {
21
+ fd = openSync(tmp, 'w', 0o600);
22
+ writeSync(fd, content, null, 'utf8');
23
+ fsyncSync(fd);
24
+ closeSync(fd);
25
+ fd = null;
26
+ renameSync(tmp, filePath);
27
+ }
28
+ catch (err) {
29
+ if (fd !== null) {
30
+ try {
31
+ closeSync(fd);
32
+ }
33
+ catch {
34
+ /* swallow — we're already on the error path */
35
+ }
36
+ }
37
+ try {
38
+ unlinkSync(tmp);
39
+ }
40
+ catch {
41
+ /* swallow — the tmp may never have been created */
42
+ }
43
+ throw err;
44
+ }
45
+ }
46
+ /** Compare two semver triplets numerically. `null` represents the legacy
47
+ * unversioned marker — the block existed before we added versioning, so
48
+ * it is older than any real VERSION. Returns negative when `a < b`. */
49
+ function compareSemver(a, b) {
50
+ if (a === null)
51
+ return -1;
52
+ const pa = a.split('.').map(Number);
53
+ const pb = b.split('.').map(Number);
54
+ for (let i = 0; i < 3; i++) {
55
+ const da = pa[i] ?? 0;
56
+ const db = pb[i] ?? 0;
57
+ if (da !== db)
58
+ return da - db;
59
+ }
60
+ return 0;
61
+ }
62
+ /** Throw SymlinkRefusedError if `filePath` is a symlink. existsSync follows
63
+ * symlinks, so callers must use this lstat-based check separately. */
64
+ function guardSymlink(filePath) {
65
+ let st;
66
+ try {
67
+ st = lstatSync(filePath);
68
+ }
69
+ catch {
70
+ return; // file does not exist — not our problem here
71
+ }
72
+ if (!st.isSymbolicLink())
73
+ return;
74
+ const rawTarget = readlinkSync(filePath);
75
+ // realpath gives us the absolute resolved path even when readlink returns
76
+ // something relative. Fall back to the raw readlink output if realpath
77
+ // throws (broken symlink) so the error message still names a target.
78
+ let resolved;
79
+ try {
80
+ resolved = realpathSync(filePath);
81
+ }
82
+ catch {
83
+ resolved = rawTarget;
84
+ }
85
+ throw new SymlinkRefusedError(filePath, resolved, block());
86
+ }
87
+ export function detectAt(filePath) {
88
+ if (!existsSync(filePath)) {
89
+ return { filePath, state: { kind: 'no-file' } };
90
+ }
91
+ const text = readFileSync(filePath, 'utf8');
92
+ if (countBeginMarkers(text) > 1) {
93
+ return { filePath, state: { kind: 'corrupt', reason: 'multiple-begin-markers' } };
94
+ }
95
+ const span = findBlockSpan(text);
96
+ if (!span) {
97
+ return { filePath, state: { kind: 'not-installed' } };
98
+ }
99
+ const installedVersion = span.installedVersion;
100
+ const cmp = compareSemver(installedVersion, VERSION);
101
+ return {
102
+ filePath,
103
+ state: cmp < 0
104
+ ? { kind: 'installed-outdated', version: installedVersion }
105
+ : { kind: 'installed', version: installedVersion },
106
+ };
107
+ }
108
+ /**
109
+ * Insert or refresh the block in `filePath`. Returns a one-line description
110
+ * of what changed, suitable for surfacing to the user.
111
+ */
112
+ export function installAt(filePath, displayName) {
113
+ guardSymlink(filePath);
114
+ if (!existsSync(filePath)) {
115
+ mkdirSync(dirname(filePath), { recursive: true });
116
+ // New file: portable markdown — LF by default. We do NOT pick CRLF on
117
+ // Windows because the content is going into a tool-managed .md file
118
+ // that may be synced to other OSes.
119
+ atomicWrite(filePath, block());
120
+ return `Created ${filePath} and inserted the browser-link instructions block for ${displayName}.`;
121
+ }
122
+ const text = readFileSync(filePath, 'utf8');
123
+ if (countBeginMarkers(text) > 1) {
124
+ throw new CorruptBlockError(filePath, 'multiple-begin-markers');
125
+ }
126
+ const eol = detectEol(text);
127
+ const fresh = block(VERSION, eol);
128
+ const span = findBlockSpan(text);
129
+ if (!span) {
130
+ // Append. Ensure a blank line separator from prior content, matching
131
+ // the file's dominant line ending.
132
+ const doubleEol = `${eol}${eol}`;
133
+ const separator = text.length === 0 || text.endsWith(doubleEol) ? '' : text.endsWith(eol) ? eol : doubleEol;
134
+ atomicWrite(filePath, text + separator + fresh);
135
+ return `Appended the browser-link instructions block to ${filePath} (${displayName}).`;
136
+ }
137
+ const before = text.slice(0, span.startIndex);
138
+ const after = text.slice(span.endIndex);
139
+ atomicWrite(filePath, before + fresh + after);
140
+ const prev = span.installedVersion ?? 'unversioned';
141
+ return `Refreshed the browser-link instructions block in ${filePath} (${displayName}, was v${prev}).`;
142
+ }
143
+ export function uninstallAt(filePath, displayName) {
144
+ if (!existsSync(filePath)) {
145
+ return `No ${displayName} instructions file at ${filePath}; nothing to remove.`;
146
+ }
147
+ guardSymlink(filePath);
148
+ const text = readFileSync(filePath, 'utf8');
149
+ if (countBeginMarkers(text) > 1) {
150
+ throw new CorruptBlockError(filePath, 'multiple-begin-markers');
151
+ }
152
+ const span = findBlockSpan(text);
153
+ if (!span) {
154
+ return `browser-link instructions block was not present in ${filePath}; nothing to remove.`;
155
+ }
156
+ const before = text.slice(0, span.startIndex);
157
+ const after = text.slice(span.endIndex);
158
+ // Collapse double blank lines created by removing the block, but keep the
159
+ // user's own content. A single trailing newline at EOF is restored,
160
+ // matching whatever EOL the remaining content uses.
161
+ const remaining = before + after;
162
+ const eol = detectEol(remaining);
163
+ const collapsePattern = eol === '\r\n' ? /(?:\r\n){3,}/g : /\n{3,}/g;
164
+ const doubleEol = `${eol}${eol}`;
165
+ let next = remaining.replace(collapsePattern, doubleEol);
166
+ if (next.length > 0 && !next.endsWith(eol))
167
+ next += eol;
168
+ atomicWrite(filePath, next);
169
+ return `Removed the browser-link instructions block from ${filePath} (${displayName}).`;
170
+ }
171
+ //# sourceMappingURL=file-ops.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-ops.js","sourceRoot":"","sources":["../../src/agent-instructions/file-ops.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { ClientId, InstructionsInstaller } from './types.js';
2
+ /**
3
+ * Order matches the MCP `INSTALLERS` array so the agent-instructions screen
4
+ * and the MCP installers screen list clients in the same sequence.
5
+ */
6
+ export declare const INSTRUCTIONS_INSTALLERS: InstructionsInstaller[];
7
+ export declare function getInstructionsInstaller(id: ClientId): InstructionsInstaller;
8
+ export type { ClientId, InstructionsDetect, InstructionsInstaller, InstructionsState, } from './types.js';
9
+ export { CorruptBlockError, SymlinkRefusedError } from './errors.js';
@@ -0,0 +1,20 @@
1
+ import { claudeInstructionsInstaller } from './claude.js';
2
+ import { copilotInstructionsInstaller } from './copilot.js';
3
+ import { opencodeInstructionsInstaller } from './opencode.js';
4
+ /**
5
+ * Order matches the MCP `INSTALLERS` array so the agent-instructions screen
6
+ * and the MCP installers screen list clients in the same sequence.
7
+ */
8
+ export const INSTRUCTIONS_INSTALLERS = [
9
+ claudeInstructionsInstaller,
10
+ opencodeInstructionsInstaller,
11
+ copilotInstructionsInstaller,
12
+ ];
13
+ export function getInstructionsInstaller(id) {
14
+ const found = INSTRUCTIONS_INSTALLERS.find((i) => i.id === id);
15
+ if (!found)
16
+ throw new Error(`Unknown or unsupported client: ${id}`);
17
+ return found;
18
+ }
19
+ export { CorruptBlockError, SymlinkRefusedError } from './errors.js';
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/agent-instructions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,4BAA4B,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,6BAA6B,EAAE,MAAM,eAAe,CAAC;AAG9D;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAA4B;IAC9D,2BAA2B;IAC3B,6BAA6B;IAC7B,4BAA4B;CAC7B,CAAC;AAEF,MAAM,UAAU,wBAAwB,CAAC,EAAY;IACnD,MAAM,KAAK,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,EAAE,EAAE,CAAC,CAAC;IACpE,OAAO,KAAK,CAAC;AACf,CAAC;AASD,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { InstructionsInstaller } from './types.js';
2
+ export declare const opencodeInstructionsInstaller: InstructionsInstaller;
@@ -0,0 +1,30 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { detectAt, installAt, uninstallAt } from './file-ops.js';
4
+ /**
5
+ * OpenCode follows the AGENTS.md convention (https://agents.md). Global
6
+ * instructions live next to the MCP config at
7
+ * `~/.config/opencode/AGENTS.md` on every OS — same dir as `opencode.json`.
8
+ * Project-level AGENTS.md still applies in addition; the global file is
9
+ * the one we manage so the agent gets the trigger list everywhere.
10
+ */
11
+ function file() {
12
+ return join(homedir(), '.config', 'opencode', 'AGENTS.md');
13
+ }
14
+ export const opencodeInstructionsInstaller = {
15
+ id: 'opencode',
16
+ displayName: 'OpenCode',
17
+ filePath() {
18
+ return file();
19
+ },
20
+ detect() {
21
+ return detectAt(file());
22
+ },
23
+ install() {
24
+ return installAt(file(), 'OpenCode');
25
+ },
26
+ uninstall() {
27
+ return uninstallAt(file(), 'OpenCode');
28
+ },
29
+ };
30
+ //# sourceMappingURL=opencode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode.js","sourceRoot":"","sources":["../../src/agent-instructions/opencode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGjE;;;;;;GAMG;AACH,SAAS,IAAI;IACX,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,CAAC,MAAM,6BAA6B,GAA0B;IAClE,EAAE,EAAE,UAAU;IACd,WAAW,EAAE,UAAU;IAEvB,QAAQ;QACN,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC;IAED,MAAM;QACJ,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1B,CAAC;IAED,OAAO;QACL,OAAO,SAAS,CAAC,IAAI,EAAE,EAAE,UAAU,CAAC,CAAC;IACvC,CAAC;IAED,SAAS;QACP,OAAO,WAAW,CAAC,IAAI,EAAE,EAAE,UAAU,CAAC,CAAC;IACzC,CAAC;CACF,CAAC"}
@@ -0,0 +1,44 @@
1
+ import type { ClientId } from '../installers/types.js';
2
+ /** Re-export so consumers can stick to one ClientId import. */
3
+ export type { ClientId };
4
+ /** Possible states of the instructions block in the client's markdown file. */
5
+ export type InstructionsState = {
6
+ kind: 'no-file';
7
+ } | {
8
+ kind: 'not-installed';
9
+ } | {
10
+ kind: 'installed';
11
+ version: string | null;
12
+ } | {
13
+ kind: 'installed-outdated';
14
+ version: string | null;
15
+ } | {
16
+ kind: 'corrupt';
17
+ reason: 'multiple-begin-markers';
18
+ };
19
+ export interface InstructionsDetect {
20
+ /** Absolute path to the markdown file we manage. Returned even when it does not exist. */
21
+ filePath: string;
22
+ /** State of the block in that file. */
23
+ state: InstructionsState;
24
+ }
25
+ export interface InstructionsInstaller {
26
+ id: ClientId;
27
+ displayName: string;
28
+ /** Absolute path to the markdown file we manage for this client. */
29
+ filePath(): string;
30
+ /** Inspect the file and report the state of the browser-link block. */
31
+ detect(): InstructionsDetect;
32
+ /**
33
+ * Insert / refresh the browser-link block in the file. Idempotent: if the
34
+ * block is already present, the body is rewritten in place; the file is
35
+ * created if it does not exist. Returns a one-line description of what
36
+ * changed.
37
+ */
38
+ install(): string;
39
+ /**
40
+ * Remove the browser-link block from the file. Leaves every other line
41
+ * intact. Idempotent: a missing file or missing block is a no-op.
42
+ */
43
+ uninstall(): string;
44
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/agent-instructions/types.ts"],"names":[],"mappings":""}