patchrome 0.1.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.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +514 -0
  3. package/bin/patchrome.js +10 -0
  4. package/dist/build-id.d.ts +2 -0
  5. package/dist/build-id.js +21 -0
  6. package/dist/challenges.d.ts +22 -0
  7. package/dist/challenges.js +97 -0
  8. package/dist/chrome-profiles.d.ts +17 -0
  9. package/dist/chrome-profiles.js +141 -0
  10. package/dist/cli-options.d.ts +131 -0
  11. package/dist/cli-options.js +43 -0
  12. package/dist/cli.d.ts +48 -0
  13. package/dist/cli.js +572 -0
  14. package/dist/client.d.ts +16 -0
  15. package/dist/client.js +210 -0
  16. package/dist/commands.d.ts +58 -0
  17. package/dist/commands.js +1076 -0
  18. package/dist/completions.d.ts +1 -0
  19. package/dist/completions.js +114 -0
  20. package/dist/copy-guard.d.ts +75 -0
  21. package/dist/copy-guard.js +167 -0
  22. package/dist/daemon.d.ts +7 -0
  23. package/dist/daemon.js +313 -0
  24. package/dist/diagnostics.d.ts +44 -0
  25. package/dist/diagnostics.js +117 -0
  26. package/dist/engine.d.ts +51 -0
  27. package/dist/engine.js +257 -0
  28. package/dist/events.d.ts +41 -0
  29. package/dist/events.js +106 -0
  30. package/dist/extract.d.ts +27 -0
  31. package/dist/extract.js +62 -0
  32. package/dist/focus.d.ts +1 -0
  33. package/dist/focus.js +44 -0
  34. package/dist/glob.d.ts +4 -0
  35. package/dist/glob.js +63 -0
  36. package/dist/har.d.ts +105 -0
  37. package/dist/har.js +88 -0
  38. package/dist/history.d.ts +35 -0
  39. package/dist/history.js +277 -0
  40. package/dist/host-platform.d.ts +5 -0
  41. package/dist/host-platform.js +19 -0
  42. package/dist/host-prompts-macos.d.ts +2 -0
  43. package/dist/host-prompts-macos.js +102 -0
  44. package/dist/host-prompts-wsl.d.ts +6 -0
  45. package/dist/host-prompts-wsl.js +64 -0
  46. package/dist/host-prompts.d.ts +3 -0
  47. package/dist/host-prompts.js +25 -0
  48. package/dist/index.d.ts +17 -0
  49. package/dist/index.js +47 -0
  50. package/dist/network.d.ts +54 -0
  51. package/dist/network.js +204 -0
  52. package/dist/origin-storage.d.ts +31 -0
  53. package/dist/origin-storage.js +82 -0
  54. package/dist/paths.d.ts +17 -0
  55. package/dist/paths.js +52 -0
  56. package/dist/pipe.d.ts +9 -0
  57. package/dist/pipe.js +73 -0
  58. package/dist/profile-mode.d.ts +10 -0
  59. package/dist/profile-mode.js +42 -0
  60. package/dist/protocol-help.d.ts +34 -0
  61. package/dist/protocol-help.js +66 -0
  62. package/dist/protocol.d.ts +49 -0
  63. package/dist/protocol.js +89 -0
  64. package/dist/refs.d.ts +9 -0
  65. package/dist/refs.js +46 -0
  66. package/dist/routes.d.ts +20 -0
  67. package/dist/routes.js +106 -0
  68. package/dist/runner.d.ts +20 -0
  69. package/dist/runner.js +81 -0
  70. package/dist/session-name.d.ts +9 -0
  71. package/dist/session-name.js +50 -0
  72. package/dist/session-store.d.ts +5 -0
  73. package/dist/session-store.js +58 -0
  74. package/dist/sessions.d.ts +47 -0
  75. package/dist/sessions.js +171 -0
  76. package/dist/tab-groups.d.ts +9 -0
  77. package/dist/tab-groups.js +13 -0
  78. package/dist/targets.d.ts +43 -0
  79. package/dist/targets.js +229 -0
  80. package/dist/validate.d.ts +3 -0
  81. package/dist/validate.js +31 -0
  82. package/dist/wait.d.ts +24 -0
  83. package/dist/wait.js +88 -0
  84. package/examples/go/go.mod +3 -0
  85. package/examples/go/main.go +104 -0
  86. package/examples/hn-front-page.sh +18 -0
  87. package/examples/hn-front-page.ts +24 -0
  88. package/examples/hn_front_page.py +56 -0
  89. package/extension/tab-groups/manifest.json +8 -0
  90. package/extension/tab-groups/service-worker.js +41 -0
  91. package/package.json +60 -0
  92. package/skills/patchrome/SKILL.md +74 -0
  93. package/skills/patchrome/references/commands.md +130 -0
  94. package/skills/patchrome/references/debugging.md +20 -0
  95. package/skills/patchrome/references/hard-pages.md +49 -0
  96. package/skills/patchrome/references/logins.md +46 -0
  97. package/skills/patchrome/references/scraping.md +51 -0
  98. package/skills/patchrome/references/scripting.md +79 -0
@@ -0,0 +1 @@
1
+ export declare const zshCompletionScript = "#compdef patchrome\n\n_patchrome_profile_args() {\n local index=${words[(i)--profile]}\n (( index < CURRENT )) && print -r -- --profile \"${words[index+1]}\"\n}\n\n_patchrome_sessions() {\n local -a names\n names=(${(f)\"$(patchrome $(_patchrome_profile_args) sessions 2>/dev/null | awk '$0 != \"no sessions\" {print $1}')\"})\n (( ${#names} )) && _describe -t sessions 'session' names\n}\n\n_patchrome_tabs() {\n local -a ids\n patchrome $(_patchrome_profile_args) sessions >/dev/null 2>&1 || return\n ids=(${(f)\"$(patchrome $(_patchrome_profile_args) tabs 2>/dev/null | awk '$2 ~ /^t[0-9]+$/ {print $2 \":\" $3}')\"})\n (( ${#ids} )) && _describe -t tabs 'tab' ids\n}\n\n_patchrome() {\n local -a commands\n commands=(\n 'open:new background tab, becomes current'\n 'tabs:list your tabs'\n 'switch:make a tab current'\n 'close:close a tab'\n 'goto:navigate the current tab'\n 'snapshot:accessibility tree with refs'\n 'screenshot:PNG of the current tab'\n 'text:visible text'\n 'eval:run a JS expression'\n 'extract:CSS selectors to JSON rows'\n 'wait:block until the page shows a condition'\n 'watch:stream page events'\n 'click:click a ref or locator'\n 'fill:fill a ref or locator'\n 'press:press a key'\n 'type:type text into the focused element'\n 'challenge:detect a CAPTCHA, or hand it to a person'\n 'network:list, get or record requests'\n 'route:block or mock requests'\n 'login:sign in by hand in a visible tab'\n 'cookies:list cookies'\n 'state:save, load or import login state'\n 'console:console messages (debug profile)'\n 'errors:page errors (debug profile)'\n 'trace:record a Playwright trace (debug profile)'\n 'cdp:raw CDP call or help (debug profile)'\n 'devtools-url:DevTools URL (debug profile)'\n 'profile:create a profile'\n 'session:show, label or close sessions'\n 'sessions:list sessions'\n 'daemon:status, stop or logs'\n 'audit:recent login copies and their approvals'\n 'pipe:run JSON requests from stdin'\n 'completions:print a shell completion script'\n )\n\n local -a locator_args\n locator_args=(\n '--role[ARIA role]:role:(button link textbox checkbox radio combobox option heading listitem row cell tab menuitem dialog img)'\n '--name[accessible name, with --role]:name:'\n '--text[visible text]:text:'\n '--label[form label]:text:'\n '--selector[CSS selector]:css:'\n '--exact[match the whole name or text]'\n '--nth[pick the nth match, from 0]:n:'\n '--frame[iframe CSS selector]:css:'\n )\n\n _arguments -C \\\n '--profile[browser profile]:profile:' \\\n '--session[session name]:session:_patchrome_sessions' \\\n '--json[print JSON]' \\\n '--timeout-ms[timeout in ms]:ms:' \\\n '1: :->command' \\\n '*:: :->args' && return\n\n case $state in\n command) _describe -t commands 'patchrome command' commands ;;\n args)\n case $words[1] in\n switch|close) _patchrome_tabs ;;\n sessions) _patchrome_sessions ;;\n session)\n if (( CURRENT == 2 )); then _values 'action' label close history\n elif [[ $words[2] == history ]]; then _arguments '--format[script format]:format:(sh jsonl)' '--out[write to a file]:file:_files'\n elif [[ $words[2] == close ]]; then _patchrome_sessions\n fi\n ;;\n network) (( CURRENT == 2 )) && _values 'action' list get har ;;\n route) (( CURRENT == 2 )) && _values 'action' block mock list clear ;;\n state) (( CURRENT == 2 )) && _values 'action' save load import ;;\n trace) (( CURRENT == 2 )) && _values 'action' start stop ;;\n daemon) (( CURRENT == 2 )) && _values 'action' status stop logs ;;\n cdp) (( CURRENT == 2 )) && _values 'action' help ;;\n completions) _values 'shell' zsh ;;\n wait) _arguments $locator_args '--url[URL glob]:glob:' '--title[title text]:text:' '--gone[wait for it to go away]' '--load[load state]:state:(load domcontentloaded networkidle)' ;;\n click) _arguments $locator_args '--at[viewport point]:x,y:' ;;\n fill) _arguments $locator_args ;;\n text|screenshot|extract|eval) _arguments $locator_args '--inline[print the value]' '--out[write to a file]:file:_files' ;;\n pipe) _arguments '--bail[stop at the first failure]' ;;\n challenge) _arguments '--handoff[raise the tab and wait for a person]' ;;\n watch) _arguments '--events[event kinds]:events:_values -s , event navigation load response console error' '--url[URL glob]:glob:' '--count[stop after n events]:n:' ;;\n esac\n ;;\n esac\n}\n\ncompdef _patchrome patchrome\n";
@@ -0,0 +1,114 @@
1
+ // Printed by `patchrome completions zsh`. Session names and tab ids come from the running daemon as
2
+ // you type; with no daemon running they are left out, and completion never starts Chrome.
3
+ export const zshCompletionScript = `#compdef patchrome
4
+
5
+ _patchrome_profile_args() {
6
+ local index=\${words[(i)--profile]}
7
+ (( index < CURRENT )) && print -r -- --profile "\${words[index+1]}"
8
+ }
9
+
10
+ _patchrome_sessions() {
11
+ local -a names
12
+ names=(\${(f)"$(patchrome $(_patchrome_profile_args) sessions 2>/dev/null | awk '$0 != "no sessions" {print $1}')"})
13
+ (( \${#names} )) && _describe -t sessions 'session' names
14
+ }
15
+
16
+ _patchrome_tabs() {
17
+ local -a ids
18
+ patchrome $(_patchrome_profile_args) sessions >/dev/null 2>&1 || return
19
+ ids=(\${(f)"$(patchrome $(_patchrome_profile_args) tabs 2>/dev/null | awk '$2 ~ /^t[0-9]+$/ {print $2 ":" $3}')"})
20
+ (( \${#ids} )) && _describe -t tabs 'tab' ids
21
+ }
22
+
23
+ _patchrome() {
24
+ local -a commands
25
+ commands=(
26
+ 'open:new background tab, becomes current'
27
+ 'tabs:list your tabs'
28
+ 'switch:make a tab current'
29
+ 'close:close a tab'
30
+ 'goto:navigate the current tab'
31
+ 'snapshot:accessibility tree with refs'
32
+ 'screenshot:PNG of the current tab'
33
+ 'text:visible text'
34
+ 'eval:run a JS expression'
35
+ 'extract:CSS selectors to JSON rows'
36
+ 'wait:block until the page shows a condition'
37
+ 'watch:stream page events'
38
+ 'click:click a ref or locator'
39
+ 'fill:fill a ref or locator'
40
+ 'press:press a key'
41
+ 'type:type text into the focused element'
42
+ 'challenge:detect a CAPTCHA, or hand it to a person'
43
+ 'network:list, get or record requests'
44
+ 'route:block or mock requests'
45
+ 'login:sign in by hand in a visible tab'
46
+ 'cookies:list cookies'
47
+ 'state:save, load or import login state'
48
+ 'console:console messages (debug profile)'
49
+ 'errors:page errors (debug profile)'
50
+ 'trace:record a Playwright trace (debug profile)'
51
+ 'cdp:raw CDP call or help (debug profile)'
52
+ 'devtools-url:DevTools URL (debug profile)'
53
+ 'profile:create a profile'
54
+ 'session:show, label or close sessions'
55
+ 'sessions:list sessions'
56
+ 'daemon:status, stop or logs'
57
+ 'audit:recent login copies and their approvals'
58
+ 'pipe:run JSON requests from stdin'
59
+ 'completions:print a shell completion script'
60
+ )
61
+
62
+ local -a locator_args
63
+ locator_args=(
64
+ '--role[ARIA role]:role:(button link textbox checkbox radio combobox option heading listitem row cell tab menuitem dialog img)'
65
+ '--name[accessible name, with --role]:name:'
66
+ '--text[visible text]:text:'
67
+ '--label[form label]:text:'
68
+ '--selector[CSS selector]:css:'
69
+ '--exact[match the whole name or text]'
70
+ '--nth[pick the nth match, from 0]:n:'
71
+ '--frame[iframe CSS selector]:css:'
72
+ )
73
+
74
+ _arguments -C \\
75
+ '--profile[browser profile]:profile:' \\
76
+ '--session[session name]:session:_patchrome_sessions' \\
77
+ '--json[print JSON]' \\
78
+ '--timeout-ms[timeout in ms]:ms:' \\
79
+ '1: :->command' \\
80
+ '*:: :->args' && return
81
+
82
+ case $state in
83
+ command) _describe -t commands 'patchrome command' commands ;;
84
+ args)
85
+ case $words[1] in
86
+ switch|close) _patchrome_tabs ;;
87
+ sessions) _patchrome_sessions ;;
88
+ session)
89
+ if (( CURRENT == 2 )); then _values 'action' label close history
90
+ elif [[ $words[2] == history ]]; then _arguments '--format[script format]:format:(sh jsonl)' '--out[write to a file]:file:_files'
91
+ elif [[ $words[2] == close ]]; then _patchrome_sessions
92
+ fi
93
+ ;;
94
+ network) (( CURRENT == 2 )) && _values 'action' list get har ;;
95
+ route) (( CURRENT == 2 )) && _values 'action' block mock list clear ;;
96
+ state) (( CURRENT == 2 )) && _values 'action' save load import ;;
97
+ trace) (( CURRENT == 2 )) && _values 'action' start stop ;;
98
+ daemon) (( CURRENT == 2 )) && _values 'action' status stop logs ;;
99
+ cdp) (( CURRENT == 2 )) && _values 'action' help ;;
100
+ completions) _values 'shell' zsh ;;
101
+ wait) _arguments $locator_args '--url[URL glob]:glob:' '--title[title text]:text:' '--gone[wait for it to go away]' '--load[load state]:state:(load domcontentloaded networkidle)' ;;
102
+ click) _arguments $locator_args '--at[viewport point]:x,y:' ;;
103
+ fill) _arguments $locator_args ;;
104
+ text|screenshot|extract|eval) _arguments $locator_args '--inline[print the value]' '--out[write to a file]:file:_files' ;;
105
+ pipe) _arguments '--bail[stop at the first failure]' ;;
106
+ challenge) _arguments '--handoff[raise the tab and wait for a person]' ;;
107
+ watch) _arguments '--events[event kinds]:events:_values -s , event navigation load response console error' '--url[URL glob]:glob:' '--count[stop after n events]:n:' ;;
108
+ esac
109
+ ;;
110
+ esac
111
+ }
112
+
113
+ compdef _patchrome patchrome
114
+ `;
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ export declare const copyKinds: readonly ["state-import", "state-load", "state-save"];
3
+ export type CopyKind = (typeof copyKinds)[number];
4
+ export declare const approvalAnswers: readonly ["approved", "denied", "timed_out", "unavailable"];
5
+ export type ApprovalAnswer = (typeof approvalAnswers)[number];
6
+ export declare const copyDecisions: readonly ["approved", "denied", "timed_out", "unavailable", "not_asked"];
7
+ export type CopyDecision = (typeof copyDecisions)[number];
8
+ export declare const approvalTimeoutMs = 60000;
9
+ export interface CopyRequest {
10
+ kind: CopyKind;
11
+ session: string;
12
+ source: string;
13
+ target: string;
14
+ site: string | undefined;
15
+ cookies: number;
16
+ origins: string[];
17
+ }
18
+ export interface HostPrompts {
19
+ askApproval(reason: string, timeoutMs: number): Promise<{
20
+ answer: ApprovalAnswer;
21
+ detail: string | undefined;
22
+ }>;
23
+ notify(title: string, body: string): Promise<void>;
24
+ }
25
+ declare const auditEntrySchema: z.ZodObject<{
26
+ atUtc: z.ZodString;
27
+ profile: z.ZodString;
28
+ kind: z.ZodEnum<{
29
+ "state-import": "state-import";
30
+ "state-load": "state-load";
31
+ "state-save": "state-save";
32
+ }>;
33
+ session: z.ZodString;
34
+ source: z.ZodString;
35
+ target: z.ZodString;
36
+ site: z.ZodOptional<z.ZodString>;
37
+ cookies: z.ZodNumber;
38
+ origins: z.ZodArray<z.ZodString>;
39
+ decision: z.ZodEnum<{
40
+ approved: "approved";
41
+ denied: "denied";
42
+ not_asked: "not_asked";
43
+ timed_out: "timed_out";
44
+ unavailable: "unavailable";
45
+ }>;
46
+ detail: z.ZodOptional<z.ZodString>;
47
+ }, z.core.$strip>;
48
+ export type AuditEntry = z.infer<typeof auditEntrySchema>;
49
+ export interface CopyGuardOptions {
50
+ prompts: HostPrompts;
51
+ auditLogPath: string;
52
+ profile: string;
53
+ log: (message: string) => void;
54
+ nowMs?: () => number;
55
+ }
56
+ export declare class CopyGuard {
57
+ #private;
58
+ constructor(options: CopyGuardOptions);
59
+ requireApproval(request: CopyRequest): Promise<void>;
60
+ recordUnasked(request: CopyRequest): Promise<void>;
61
+ }
62
+ export declare function shouldNotify(decision: CopyDecision): boolean;
63
+ export declare function describeCopy(request: CopyRequest): string;
64
+ export declare function approvalReason(request: CopyRequest): string;
65
+ export declare function helperFailure(helper: string, err: unknown): string;
66
+ export declare function parseApprovalAnswer(stdout: string): {
67
+ answer: ApprovalAnswer;
68
+ detail: string | undefined;
69
+ };
70
+ export declare function readAuditLog(path: string): Promise<{
71
+ entries: AuditEntry[];
72
+ unreadableLines: number[];
73
+ }>;
74
+ export declare function auditLine(entry: AuditEntry): string;
75
+ export {};
@@ -0,0 +1,167 @@
1
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname } from "node:path";
4
+ import { z } from "zod";
5
+ import { CommandError } from "./protocol.js";
6
+ // Commands that move logins between a patchrome profile and somewhere else. Loading data in waits for a
7
+ // person; saving it out only leaves a record, so an agent that saves often does not raise prompts.
8
+ export const copyKinds = ["state-import", "state-load", "state-save"];
9
+ // What the person answered, or why nobody could be asked.
10
+ export const approvalAnswers = ["approved", "denied", "timed_out", "unavailable"];
11
+ export const copyDecisions = [...approvalAnswers, "not_asked"];
12
+ export const approvalTimeoutMs = 60_000;
13
+ const auditEntrySchema = z.object({
14
+ atUtc: z.string(),
15
+ profile: z.string(),
16
+ kind: z.enum(copyKinds),
17
+ session: z.string(),
18
+ source: z.string(),
19
+ target: z.string(),
20
+ site: z.string().optional(),
21
+ cookies: z.number(),
22
+ origins: z.array(z.string()),
23
+ decision: z.enum(copyDecisions),
24
+ detail: z.string().optional(),
25
+ });
26
+ export class CopyGuard {
27
+ #options;
28
+ // One prompt at a time, so two agents importing together never stack two Touch ID dialogs.
29
+ #turns = Promise.resolve();
30
+ constructor(options) {
31
+ this.#options = options;
32
+ }
33
+ async requireApproval(request) {
34
+ const turn = this.#turns.then(() => this.#decide(request));
35
+ this.#turns = turn.catch(() => { });
36
+ const { decision, detail } = await turn;
37
+ switch (decision) {
38
+ case "approved":
39
+ return;
40
+ case "denied":
41
+ case "timed_out":
42
+ case "unavailable":
43
+ throw new CommandError("copy_denied", `${describeCopy(request)} was not approved: ${decision.replace("_", " ")}${detail === undefined ? "" : `, ${detail}`}`, "the user must approve this copy at the prompt; tell them, and do not retry or copy the data another way");
44
+ case "not_asked":
45
+ throw new Error(`a ${request.kind} approval cannot end as not_asked`);
46
+ }
47
+ }
48
+ async recordUnasked(request) {
49
+ await this.#record(request, "not_asked", undefined);
50
+ await this.#notify(request, "not_asked");
51
+ }
52
+ async #decide(request) {
53
+ const { answer: decision, detail } = await this.#options.prompts
54
+ .askApproval(approvalReason(request), approvalTimeoutMs)
55
+ .catch((err) => ({
56
+ answer: "unavailable",
57
+ detail: err instanceof Error ? err.message.split("\n")[0] : String(err),
58
+ }));
59
+ // Fails the command when the record cannot be written: an unrecorded copy is what the log exists to prevent.
60
+ await this.#record(request, decision, detail);
61
+ await this.#notify(request, decision);
62
+ return { decision, detail };
63
+ }
64
+ async #record(request, decision, detail) {
65
+ const entry = {
66
+ atUtc: new Date((this.#options.nowMs ?? Date.now)()).toISOString(),
67
+ profile: this.#options.profile,
68
+ kind: request.kind,
69
+ session: request.session,
70
+ source: request.source,
71
+ target: request.target,
72
+ ...(request.site === undefined ? {} : { site: request.site }),
73
+ cookies: request.cookies,
74
+ origins: request.origins,
75
+ decision,
76
+ ...(detail === undefined ? {} : { detail }),
77
+ };
78
+ await mkdir(dirname(this.#options.auditLogPath), { recursive: true });
79
+ await appendFile(this.#options.auditLogPath, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
80
+ this.#options.log(`copy ${decision}: ${describeCopy(request)} for ${request.session}`);
81
+ }
82
+ // A person who just answered the prompt already knows; everything else gets a notification.
83
+ async #notify(request, decision) {
84
+ if (!shouldNotify(decision))
85
+ return;
86
+ await this.#options.prompts
87
+ .notify(`patchrome copy ${decision.replace("_", " ")}`, `${request.session}: ${describeCopy(request)}`)
88
+ .catch((err) => this.#options.log(`copy notification failed: ${String(err).split("\n")[0]}`));
89
+ }
90
+ }
91
+ export function shouldNotify(decision) {
92
+ switch (decision) {
93
+ case "approved":
94
+ case "denied":
95
+ return false;
96
+ case "timed_out":
97
+ case "unavailable":
98
+ case "not_asked":
99
+ return true;
100
+ }
101
+ }
102
+ export function describeCopy(request) {
103
+ const counts = [
104
+ plural(request.cookies, "cookie"),
105
+ ...(request.origins.length === 0 ? [] : [`storage for ${plural(request.origins.length, "origin")}`]),
106
+ ].join(" and ");
107
+ const what = request.site === undefined ? counts : `the ${request.site} login (${counts})`;
108
+ switch (request.kind) {
109
+ case "state-import":
110
+ return `import ${what} from ${request.source} into ${request.target}`;
111
+ case "state-load":
112
+ return `load ${what} from ${request.source} into ${request.target}`;
113
+ case "state-save":
114
+ return `save ${what} from ${request.source} to ${request.target}`;
115
+ }
116
+ }
117
+ // macOS shows this after "patchrome is trying to", Windows Hello shows it as the dialog's message.
118
+ export function approvalReason(request) {
119
+ const home = homedir();
120
+ const described = describeCopy(request).replaceAll(`${home}/`, "~/");
121
+ return `${described}. Asked by agent session ${request.session}.`;
122
+ }
123
+ function plural(count, noun) {
124
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
125
+ }
126
+ // A killed or missing helper says why in one line, without echoing the script it was given.
127
+ export function helperFailure(helper, err) {
128
+ const failure = err;
129
+ if (failure.code === "ENOENT")
130
+ return `${helper} not found`;
131
+ if (failure.killed === true || failure.signal)
132
+ return `${helper} stopped by ${failure.signal ?? "timeout"}`;
133
+ const stderr = (err.stderr ?? "").toString().trim().split("\n")[0];
134
+ return `${helper} exited with ${String(failure.code)}${stderr ? `: ${stderr}` : ""}`;
135
+ }
136
+ // Platform helpers print one answer word, optionally followed by a detail.
137
+ export function parseApprovalAnswer(stdout) {
138
+ const text = stdout.trim();
139
+ const [word = "", ...rest] = text.split(/\s+/);
140
+ const answer = approvalAnswers.find((candidate) => candidate === word);
141
+ if (answer === undefined)
142
+ return { answer: "unavailable", detail: `unreadable prompt answer: ${text.slice(0, 200)}` };
143
+ return { answer, detail: rest.length === 0 ? undefined : rest.join(" ") };
144
+ }
145
+ export async function readAuditLog(path) {
146
+ const raw = await readFile(path, "utf8").catch((err) => {
147
+ if (err.code === "ENOENT")
148
+ return "";
149
+ throw err;
150
+ });
151
+ const entries = [];
152
+ const unreadableLines = [];
153
+ raw.split("\n").forEach((line, index) => {
154
+ if (line.trim() === "")
155
+ return;
156
+ try {
157
+ entries.push(auditEntrySchema.parse(JSON.parse(line)));
158
+ }
159
+ catch {
160
+ unreadableLines.push(index + 1);
161
+ }
162
+ });
163
+ return { entries, unreadableLines };
164
+ }
165
+ export function auditLine(entry) {
166
+ return `${entry.atUtc} ${entry.decision} ${entry.session}: ${describeCopy({ ...entry, site: entry.site })}${entry.detail === undefined ? "" : ` (${entry.detail})`}`;
167
+ }
@@ -0,0 +1,7 @@
1
+ import { type HostPrompts } from "./copy-guard.ts";
2
+ export interface DaemonOptions {
3
+ prompts?: HostPrompts;
4
+ exitProcess?: (code: number) => void;
5
+ }
6
+ export declare function runDaemon(profile: string, env?: NodeJS.ProcessEnv, options?: DaemonOptions): Promise<void>;
7
+ export declare function isClosedTargetRejection(reason: unknown): boolean;