@hraness/slopcamera 3.4.0 → 3.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.
Files changed (45) hide show
  1. package/README.md +32 -19
  2. package/apps/desktop/application/operation.ts +1 -1
  3. package/apps/desktop/cli/args.ts +3 -1
  4. package/apps/desktop/cli/commands.ts +22 -1
  5. package/apps/desktop/cli/help.ts +37 -13
  6. package/apps/desktop/cli/menubar-status.ts +155 -0
  7. package/apps/desktop/cli/menubar.ts +129 -101
  8. package/apps/desktop/dist/cli/main.js +334 -312
  9. package/apps/desktop/html-overlay/catalog.ts +1 -0
  10. package/apps/desktop/html-overlay/spatial.ts +10 -6
  11. package/apps/desktop/workflows/index.ts +1 -0
  12. package/dist/cli.js +7 -8
  13. package/dist/generate.js +1 -1
  14. package/dist/{index-qtn3s421.js → index-4z9y2wa2.js} +1 -1
  15. package/dist/{index-sy1n4zfy.js → index-fevf4ppk.js} +1 -1
  16. package/dist/{index-qry58nj2.js → index-mnp0bftq.js} +2 -2
  17. package/dist/index-nxtmy18t.js +3 -0
  18. package/dist/{index-abnk4h9c.js → index-qxfexvj3.js} +2 -2
  19. package/dist/index.js +1 -1
  20. package/dist/operations.js +1 -1
  21. package/dist/vectorize/worker.js +1 -1
  22. package/dist/workflow.js +1 -1
  23. package/docs/README.md +3 -3
  24. package/examples/studio/vgpu/README.md +1 -1
  25. package/package.json +5 -6
  26. package/skills/slopcamera/SKILL.md +4 -3
  27. package/skills/slopcamera/references/brand-illustrations.md +237 -0
  28. package/skills/slopcamera/references/gateway-media.md +1 -1
  29. package/skills/slopcamera/references/install.md +10 -1
  30. package/skills/slopcamera/references/patent-drawings.md +4 -4
  31. package/skills/slopcamera/references/support.md +1 -1
  32. package/skills/slopcamera/references/vectorization.md +3 -1
  33. package/src/cli.ts +13 -10
  34. package/src/code/public-operations.ts +9 -0
  35. package/src/generate.ts +44 -1
  36. package/src/generation-pricing.ts +217 -0
  37. package/src/mcp/tools.ts +4 -0
  38. package/src/operations.ts +4 -0
  39. package/src/support.ts +7 -1
  40. package/src/vectorize/command.ts +6 -1
  41. package/src/vectorize/supervisor.ts +23 -3
  42. package/src/vectorize/tool.ts +8 -4
  43. package/src/version.ts +1 -1
  44. package/src/visual-style.ts +4 -1
  45. package/dist/index-77fjfg9f.js +0 -3
@@ -7,6 +7,7 @@ import { writeJson, writeLine } from "./io";
7
7
  import { ensurePrivateDirectory } from "./paths";
8
8
 
9
9
  const SETTLE_MS = 400;
10
+ /** The label earlier releases used; the menu bar now manages its own login item. */
10
11
  const LABEL = "com.hraness.slopcamera.menubar";
11
12
 
12
13
  function xml(value: string): string { return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;"); }
@@ -109,133 +110,160 @@ function readAgent(path: string, environment: Environment): string | null {
109
110
  return readRegular(path, 16 * 1024).toString("utf8");
110
111
  }
111
112
 
112
- /** Configuration state only; it does not claim the launchd service is running. */
113
- export type LaunchAgentState = "absent" | "installed" | "conflict";
114
- export type LaunchctlRunner = (args: readonly string[], allowMissing?: boolean) => string | null;
115
- function launchctl(args: readonly string[], allowMissing = false): string | null {
116
- const uid = process.getuid?.();
117
- if (uid === undefined) throw new CliError("unavailable", "The current user has no launchd GUI domain.");
118
- const result = Bun.spawnSync(["/bin/launchctl", ...args.map((arg) => arg.replaceAll("{uid}", String(uid)))], { stdin: "ignore", stdout: "pipe", stderr: "pipe", timeout: 10_000, maxBuffer: 64 * 1024 });
119
- return interpretLaunchctlResult(result, uid, allowMissing);
120
- }
121
- export function interpretLaunchctlResult(result: { success: boolean; exitCode: number; stdout: Uint8Array; stderr: Uint8Array }, uid: number, allowMissing: boolean): string | null {
122
- if (result.stdout.byteLength + result.stderr.byteLength > 64 * 1024) throw new CliError("unavailable", "launchctl exceeded the bounded response size.");
123
- if (result.success && result.exitCode === 0) return Buffer.from(result.stdout).toString();
124
- if (allowMissing && result.exitCode === 113 && result.stdout.length === 0 && Buffer.from(result.stderr).toString() === `Bad request.\nCould not find service "${LABEL}" in domain for user gui: ${uid}\n`) return null;
125
- throw new CliError("unavailable", "launchctl could not reconcile the Slopcamera menu-bar LaunchAgent. Run menubar install again to retry.");
126
- }
127
-
128
- function loadedOwned(path: string, binary: string, current: string | null, run: LaunchctlRunner): boolean {
129
- const output = run(["print", `gui/{uid}/${LABEL}`], true);
130
- if (output === null) return false;
131
- const uid = process.getuid?.();
132
- if (current === null || Buffer.byteLength(output) > 64 * 1024 || !output.startsWith(`gui/${uid}/${LABEL} = {\n`)) {
133
- throw new CliError("conflict", "The loaded menu-bar service has an unverified identity.");
134
- }
135
- const lines = output.split("\n").map(line => line.trim());
136
- const field = (key: string): string | null => {
137
- const values = lines.filter(line => line.startsWith(`${key} = `));
138
- return values.length === 1 ? values[0]!.slice(key.length + 3) : null;
139
- };
140
- const argumentsStart = lines.indexOf("arguments = {");
141
- const argumentsEnd = lines.indexOf("}", argumentsStart + 1);
142
- if (field("path") !== path || field("program") !== binary || argumentsStart < 0 || argumentsEnd !== argumentsStart + 2 || lines[argumentsStart + 1] !== binary) {
143
- throw new CliError("conflict", "The loaded menu-bar service does not match this install; it was left untouched.");
144
- }
145
- return true;
146
- }
147
- export function launchAgentState(binary: string, environment: Environment = process.env): LaunchAgentState {
148
- const content = readAgent(launchAgentPath(environment), environment);
149
- if (content === null) return "absent";
150
- checkParents(binary, environment);
151
- assertOwnedFile(binary);
152
- return content === launchAgentPlist(binary) && exists(binary) ? "installed" : "conflict";
153
- }
154
- function assertMac(platform: NodeJS.Platform): void {
155
- if (platform !== "darwin") throw new CliError("unsupported-plan", "Slopcamera menu-bar LaunchAgents are supported only on macOS.");
156
- }
157
- export function installLaunchAgent(binary: string, environment: Environment = process.env, platform: NodeJS.Platform = process.platform, runLaunchctl: LaunchctlRunner = launchctl): LaunchAgentState {
158
- assertMac(platform);
159
- if (!isAbsolute(binary)) throw new CliError("unsafe-path", "LaunchAgent binary must be an absolute path.");
113
+ /**
114
+ * Copies the built menu bar into its installed location, replacing an
115
+ * earlier copy. Existing shared directory permissions are preserved;
116
+ * symbolic links and files other users can write are refused.
117
+ */
118
+ export function installBinary(binary: string, environment: Environment = process.env): string {
119
+ if (!isAbsolute(binary)) throw new CliError("unsafe-path", "The menu-bar build must be an absolute path.");
160
120
  const stable = installedBinaryPath(environment);
161
- const path = launchAgentPath(environment);
162
- const current = readAgent(path, environment);
163
- const expected = launchAgentPlist(stable);
164
121
  checkParents(stable, environment);
165
122
  assertOwnedFile(stable);
166
- if (current !== null && current !== expected || current === null && infoAt(stable) !== null) {
167
- throw new CliError("conflict", "The existing Slopcamera menu-bar install is not owned by this command.");
168
- }
169
123
  const bytes = readRegular(binary, MAX_BINARY_BYTES, true);
170
- loadedOwned(path, stable, current, runLaunchctl);
171
- checkParents(path, environment, true);
172
124
  checkParents(stable, environment, true);
173
- // Unique private staging directories prevent predictable temporary-file clobbering.
174
- const binaryStage = mkdtempSync(join(dirname(stable), ".install-"));
175
- let agentStage: string | undefined;
125
+ const stage = mkdtempSync(join(dirname(stable), ".install-"));
176
126
  try {
177
- agentStage = mkdtempSync(join(dirname(path), ".slopcamera-install-"));
178
- const stagedBinary = join(binaryStage, "binary");
179
- const stagedAgent = join(agentStage, "agent.plist");
180
- writeFileSync(stagedBinary, bytes, { mode: 0o700, flag: "wx" });
181
- writeFileSync(stagedAgent, expected, { mode: 0o600, flag: "wx" });
182
- if (readAgent(path, environment) !== current) throw new CliError("conflict", "The menu-bar install changed concurrently.");
127
+ const staged = join(stage, "binary");
128
+ writeFileSync(staged, bytes, { mode: 0o700, flag: "wx" });
183
129
  assertOwnedFile(stable);
184
- // Retry bootstrap even when the plist matches: a previous bootstrap may have failed.
185
- if (loadedOwned(path, stable, current, runLaunchctl)) runLaunchctl(["bootout", `gui/{uid}/${LABEL}`], true);
186
- renameSync(stagedBinary, stable);
187
- renameSync(stagedAgent, path);
188
- runLaunchctl(["bootstrap", "gui/{uid}", path]);
189
- return "installed";
130
+ renameSync(staged, stable);
131
+ return stable;
190
132
  } finally {
191
- rmSync(binaryStage, { recursive: true });
192
- if (agentStage !== undefined) rmSync(agentStage, { recursive: true });
133
+ rmSync(stage, { recursive: true });
193
134
  }
194
135
  }
195
- export function uninstallLaunchAgent(_binary: string, environment: Environment = process.env, platform: NodeJS.Platform = process.platform, runLaunchctl: LaunchctlRunner = launchctl): LaunchAgentState {
196
- assertMac(platform);
197
- const stable = installedBinaryPath(environment);
136
+
137
+ /**
138
+ * Removes the login item earlier releases wrote under the old label, but
139
+ * only when it is exactly the file they wrote. It never calls launchctl: a
140
+ * copy that is already running keeps running until you quit it or log out.
141
+ */
142
+ export function removeLegacyAgent(environment: Environment = process.env): "absent" | "removed" | "foreign" {
198
143
  const path = launchAgentPath(environment);
199
144
  const current = readAgent(path, environment);
200
145
  if (current === null) return "absent";
201
- if (current !== launchAgentPlist(stable)) throw new CliError("conflict", "The existing Slopcamera menu-bar LaunchAgent is not owned by this command.");
202
- checkParents(stable, environment);
203
- assertOwnedFile(stable);
204
- if (loadedOwned(path, stable, current, runLaunchctl)) runLaunchctl(["bootout", `gui/{uid}/${LABEL}`], true);
205
- if (readAgent(path, environment) !== current) throw new CliError("conflict", "The menu-bar install changed concurrently.");
206
- assertOwnedFile(stable);
146
+ if (current !== launchAgentPlist(installedBinaryPath(environment))) return "foreign";
207
147
  rmSync(path);
208
- if (infoAt(stable) !== null) rmSync(stable);
209
- return "absent";
148
+ return "removed";
149
+ }
150
+
151
+ function assertMac(platform: NodeJS.Platform): void {
152
+ if (platform !== "darwin") throw new CliError("unsupported-plan", "The Slopcamera menu bar runs only on macOS.");
153
+ }
154
+
155
+ export interface HelperResult { readonly code: number; readonly stdout: string; readonly stderr: string }
156
+ /** Runs the installed menu bar's own install, uninstall or status command. */
157
+ export type HelperRunner = (binary: string, args: readonly string[], env: Environment) => Promise<HelperResult>;
158
+
159
+ async function runHelper(binary: string, args: readonly string[], env: Environment): Promise<HelperResult> {
160
+ const child = Bun.spawn([binary, ...args], { stdin: "ignore", stdout: "pipe", stderr: "pipe", env: { ...env } as Record<string, string> });
161
+ const [stdout, stderr, code] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
162
+ return { code, stdout: stdout.slice(0, 64 * 1024), stderr: stderr.slice(0, 64 * 1024) };
163
+ }
164
+
165
+ const AGENT_MARKERS = ["AI_AGENT", "CLAUDECODE", "CODEX_SANDBOX", "CODEX_SANDBOX_NETWORK_DISABLED", "CURSOR_AGENT", "GEMINI_CLI"];
166
+
167
+ /**
168
+ * The helper decides between human and agent output from its environment.
169
+ * Its output is captured here, so a person's terminal is passed on as
170
+ * `HRANESS_AUDIENCE=human`; agents and explicit settings pass through.
171
+ */
172
+ export function helperEnvironment(env: Environment): Environment {
173
+ const agent = AGENT_MARKERS.some((name) => (env[name] ?? "") !== "");
174
+ return env.HRANESS_AUDIENCE !== undefined || agent ? env : { ...env, HRANESS_AUDIENCE: "human" };
175
+ }
176
+
177
+ function helperFailure(result: HelperResult): CliError {
178
+ // The helper prints the login-item notice on stderr before the error;
179
+ // the failure is the last ✗ line plus the → hint that follows it.
180
+ const lines = result.stderr.split("\n");
181
+ let start = -1;
182
+ for (const [index, line] of lines.entries()) {
183
+ const trimmed = line.trimStart();
184
+ if (trimmed.startsWith("✗") || trimmed.startsWith("FAIL")) start = index;
185
+ }
186
+ const text = (start === -1 ? result.stderr : lines.slice(start).join("\n")).trim().replace(/^(✗|FAIL)\s+/u, "");
187
+ return new CliError(result.code === 2 ? "usage" : "unavailable", text === "" ? "The Slopcamera menu bar couldn't change its login item." : text);
210
188
  }
211
- async function runBinary(binary: string, foreground: boolean): Promise<number> {
189
+
190
+ /** `slopcamera-menubar` exits with this code when another copy already holds the menu bar. */
191
+ export const ALREADY_RUNNING_EXIT = 3;
192
+ const NOT_BUILT = "The Slopcamera menu bar isn't in this release yet. Build it with cargo build --release --manifest-path desktop/Cargo.toml in a Slopcamera checkout; slopcamera finds it there, or set SLOPCAMERA_MENUBAR to the built file.";
193
+
194
+ export type Launched = "running" | "already-running" | "exited";
195
+ export type BinaryRunner = (binary: string, foreground: boolean) => Promise<number | null>;
196
+
197
+ /** Resolves the exit code, or null when a background copy is still running after startup. */
198
+ async function spawnBinary(binary: string, foreground: boolean): Promise<number | null> {
212
199
  let child: Bun.Subprocess;
213
- try { child = Bun.spawn([binary], foreground ? { stdin: "inherit", stdout: "inherit", stderr: "inherit" } : { stdin: "ignore", stdout: "ignore", stderr: "ignore" }); } catch { throw new CliError("unavailable", "The Slopcamera menu bar could not start."); }
214
- if (!foreground) { child.unref(); const settled = await Promise.race([child.exited.then((code) => code as number | null), Bun.sleep(SETTLE_MS).then(() => null)]); if (settled !== null) throw new CliError("unavailable", "The Slopcamera menu bar exited during startup."); return 0; }
200
+ try { child = Bun.spawn([binary], foreground ? { stdin: "inherit", stdout: "inherit", stderr: "inherit" } : { stdin: "ignore", stdout: "ignore", stderr: "ignore" }); } catch { throw new CliError("unavailable", "The Slopcamera menu bar couldn't start."); }
201
+ if (!foreground) { child.unref(); return await Promise.race([child.exited.then((code) => code as number | null), Bun.sleep(SETTLE_MS).then(() => null)]); }
215
202
  return await child.exited;
216
203
  }
217
- export async function launchMenubar(io: CliIo, repositoryRoot: string, asJson: boolean, mode: "foreground" | "background" = "foreground"): Promise<void> {
204
+
205
+ /** Interprets the companion's exit: another running copy is success, not a failure. */
206
+ export function launchOutcome(code: number | null, foreground: boolean): Launched {
207
+ if (code === ALREADY_RUNNING_EXIT) return "already-running";
208
+ if (code === null && !foreground) return "running";
209
+ if (code === 0 && foreground) return "exited";
210
+ throw new CliError("unavailable", "The Slopcamera menu bar stopped while starting. Run it in the foreground to see why: slopcamera menubar --foreground");
211
+ }
212
+
213
+ interface Marks { readonly ok: string; readonly warn: string; readonly note: string; readonly next: string }
214
+ /** ✓ ⚠ 🔐 → with ASCII fallbacks for dumb terminals and non-UTF-8 locales. */
215
+ export function marks(env: Readonly<Record<string, string | undefined>>): Marks {
216
+ const utf8 = [env.LC_ALL, env.LC_CTYPE, env.LANG].some((value) => value !== undefined && /utf-?8/i.test(value));
217
+ const ascii = env.TERM === "dumb" || !utf8 || env.HRANESS_ASCII === "1";
218
+ return ascii ? { ok: "OK", warn: "WARN", note: "NOTE", next: "->" } : { ok: "✓", warn: "⚠", note: "🔐", next: "→" };
219
+ }
220
+
221
+ export function launchMessage(outcome: Launched, env: Readonly<Record<string, string | undefined>>): string {
222
+ const mark = marks(env);
223
+ if (outcome === "already-running") return `${mark.ok} Slopcamera is already in your menu bar. Look for 📷.`;
224
+ if (outcome === "running") return `${mark.ok} Slopcamera is in your menu bar. Look for 📷.`;
225
+ return "The Slopcamera menu bar closed.";
226
+ }
227
+
228
+ export async function launchMenubar(io: CliIo, repositoryRoot: string, asJson: boolean, mode: "foreground" | "background" = "foreground", runBinary: BinaryRunner = spawnBinary): Promise<void> {
218
229
  const binary = resolveMenubarBinary(repositoryRoot, io.env);
219
- if (binary === null) throw new CliError("unavailable", "The Slopcamera menu bar is not installed. Build it separately or set SLOPCAMERA_MENUBAR.");
220
- const code = await runBinary(binary, mode === "foreground");
221
- if (code !== 0) throw new CliError("unavailable", "The Slopcamera menu bar exited during startup.");
222
- if (asJson) writeJson(io, { running: mode === "background", foreground: mode === "foreground" });
223
- else writeLine(io, mode === "foreground" ? "Slopcamera menu bar exited." : "Slopcamera menu bar is running.");
230
+ if (binary === null) throw new CliError("unavailable", NOT_BUILT);
231
+ const outcome = launchOutcome(await runBinary(binary, mode === "foreground"), mode === "foreground");
232
+ if (asJson) writeJson(io, { running: outcome !== "exited", foreground: mode === "foreground", alreadyRunning: outcome === "already-running" });
233
+ else writeLine(io, launchMessage(outcome, io.env));
224
234
  }
225
- export async function manageMenubar(io: CliIo, repositoryRoot: string, action: "install" | "uninstall" | "status", asJson: boolean): Promise<void> {
235
+ export async function manageMenubar(io: CliIo, repositoryRoot: string, action: "install" | "uninstall" | "status", asJson: boolean, run: HelperRunner = runHelper): Promise<void> {
226
236
  assertMac(io.platform);
227
237
  const stable = installedBinaryPath(io.env);
228
- let state: LaunchAgentState;
238
+ const args = asJson ? [action, "--json"] : [action];
239
+ const env = helperEnvironment(io.env);
240
+ const forward = (result: HelperResult): void => {
241
+ if (result.stdout !== "") io.stdout(result.stdout);
242
+ if (result.code !== 0) throw helperFailure(result);
243
+ if (result.stderr !== "") io.stderr(result.stderr);
244
+ };
229
245
  if (action === "install") {
230
246
  const binary = resolveMenubarBinary(repositoryRoot, io.env);
231
- if (binary === null) throw new CliError("unavailable", "The Slopcamera menu bar is not installed. Build it separately or set SLOPCAMERA_MENUBAR.");
232
- state = installLaunchAgent(binary, io.env, io.platform);
233
- } else {
234
- state = action === "uninstall" ? uninstallLaunchAgent(stable, io.env, io.platform) : launchAgentState(stable, io.env);
247
+ if (binary === null) throw new CliError("unavailable", NOT_BUILT);
248
+ const installed = binary === stable ? stable : installBinary(binary, io.env);
249
+ forward(await run(installed, args, env));
250
+ // The old login item goes only after the new one is in place, so a
251
+ // failed install never leaves nothing to start at login.
252
+ removeLegacyAgent(io.env);
253
+ return;
235
254
  }
236
- if (asJson) writeJson(io, { launchAgent: state });
237
- else writeLine(io, `Slopcamera menu-bar LaunchAgent: ${state}.`);
255
+ if (action === "uninstall") removeLegacyAgent(io.env);
256
+ if (!exists(stable)) {
257
+ if (asJson) writeJson(io, { login: "off", running: false, installed: false });
258
+ else if (action === "uninstall") writeLine(io, `${marks(io.env).ok} Slopcamera won't open at login.`);
259
+ else writeLine(io, `Slopcamera's menu bar isn't installed.\n${marks(io.env).next} slopcamera menubar install`);
260
+ return;
261
+ }
262
+ const result = await run(stable, args, env);
263
+ forward(result);
264
+ if (action === "uninstall") rmSync(stable);
238
265
  }
266
+
239
267
  export async function reportOutputsRoot(io: CliIo, stateRoot: string, asJson: boolean): Promise<void> {
240
268
  const directory = outputsRoot(stateRoot);
241
269
  await ensurePrivateDirectory(directory);