@hraness/slopcamera 3.2.7 → 3.2.8

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 (67) hide show
  1. package/NOTICE.md +1 -7
  2. package/PRIVACY.md +16 -7
  3. package/README.md +37 -12
  4. package/apps/desktop/README.md +14 -130
  5. package/apps/desktop/application/context.ts +0 -9
  6. package/apps/desktop/application/default-registry.ts +0 -10
  7. package/apps/desktop/application/operation.ts +1 -5
  8. package/apps/desktop/application/operations/index.ts +0 -1
  9. package/apps/desktop/cli/args.ts +14 -86
  10. package/apps/desktop/cli/command-host-resources.ts +2 -1
  11. package/apps/desktop/cli/commands.ts +28 -67
  12. package/apps/desktop/cli/help.ts +12 -18
  13. package/apps/desktop/cli/html-scene.ts +139 -28
  14. package/apps/desktop/cli/io.ts +2 -1
  15. package/apps/desktop/cli/main.ts +23 -47
  16. package/apps/desktop/cli/menubar.ts +244 -0
  17. package/apps/desktop/cli/music-analysis-service.ts +61 -0
  18. package/apps/desktop/cli/portable-surface.ts +12 -4
  19. package/apps/desktop/code/application-node-planner.ts +0 -47
  20. package/apps/desktop/code/host-source-layout.ts +46 -0
  21. package/apps/desktop/code/public.ts +0 -1
  22. package/apps/desktop/code/runtime-identity.ts +50 -8
  23. package/apps/desktop/code/semantic-builder.ts +0 -43
  24. package/apps/desktop/code/source-bundle.ts +23 -6
  25. package/apps/desktop/code/source-typecheck.ts +131 -43
  26. package/apps/desktop/code/worker-client.ts +14 -2
  27. package/apps/desktop/core/music-analysis.ts +124 -1
  28. package/apps/desktop/dist/cli/main.js +244 -250
  29. package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
  30. package/apps/desktop/html-overlay/index.ts +9 -0
  31. package/apps/desktop/html-overlay/runtime.ts +2 -0
  32. package/apps/desktop/html-overlay/scene.ts +29 -0
  33. package/dist/cli.js +83 -81
  34. package/dist/{index-rvghzt7b.js → index-7131pg1c.js} +43 -236
  35. package/dist/{index-mcy8z0br.js → index-fava6pge.js} +1 -1
  36. package/dist/{index-z7239b4h.js → index-h1k0fnjq.js} +1 -1
  37. package/dist/{index-p63wavx0.js → index-zfnddgay.js} +1 -1
  38. package/dist/index.js +4 -18
  39. package/dist/operations.js +2 -2
  40. package/dist/vectorize/worker.js +1 -1
  41. package/dist/workflow.js +3 -3
  42. package/docs/README.md +1 -1
  43. package/docs/studio.md +1 -1
  44. package/package.json +6 -21
  45. package/skills/slopcamera/SKILL.md +8 -0
  46. package/skills/slopcamera/references/diagrams.md +5 -14
  47. package/skills/slopcamera/references/install.md +1 -1
  48. package/skills/slopcamera/references/music-video.md +23 -3
  49. package/skills/slopcamera/references/reference-led-3d.md +1 -1
  50. package/skills/slopcamera/references/support.md +25 -0
  51. package/skills/slopcamera/references/web-media-excerpts.md +120 -0
  52. package/src/cli.ts +28 -98
  53. package/src/index.ts +0 -23
  54. package/src/support-completion.ts +16 -0
  55. package/src/support.ts +42 -0
  56. package/src/version.ts +1 -1
  57. package/apps/desktop/application/operations/recording/index.ts +0 -5
  58. package/apps/desktop/application/operations/recording/pause.ts +0 -34
  59. package/apps/desktop/application/operations/recording/resume.ts +0 -34
  60. package/apps/desktop/application/operations/recording/shared.ts +0 -199
  61. package/apps/desktop/application/operations/recording/start.ts +0 -169
  62. package/apps/desktop/application/operations/recording/stop.ts +0 -34
  63. package/apps/desktop/capture/protocol.ts +0 -689
  64. package/apps/desktop/cli/capture-bundle.ts +0 -1117
  65. package/apps/desktop/cli/recording-controller.ts +0 -1567
  66. package/apps/desktop/cli/recording-daemon.ts +0 -931
  67. package/src/desktop.ts +0 -253
@@ -0,0 +1,127 @@
1
+ import { z } from "zod";
2
+
3
+ import type { AudioReactivityBands } from "../core/music-analysis";
4
+
5
+ export const HTML_AUDIO_REACTIVITY_RESOURCE_NAME = "audio-reactivity";
6
+ export const HTML_AUDIO_REACTIVITY_RESOURCE_URL_PATH = "slopcamera/audio-reactivity.json";
7
+ export const HTML_AUDIO_REACTIVITY_MAX_BYTES = 4 * 1024 * 1024;
8
+
9
+ export type HtmlOverlayAudioReactivity = Readonly<{
10
+ kind: "slopcamera.audio-reactivity";
11
+ schemaVersion: 1;
12
+ profile: "bands-v1";
13
+ source: Readonly<{ bytes: number; sha256: string }>;
14
+ pcm: Readonly<{ sampleCount: number; sha256: string }>;
15
+ durationUs: number;
16
+ rateHz: 60;
17
+ bands: AudioReactivityBands;
18
+ }>;
19
+
20
+ export type HtmlOverlayAudioSample = Readonly<{
21
+ bass: number; mid: number; treble: number; energy: number;
22
+ }>;
23
+ export type PreparedHtmlOverlayAudioReactivity = Readonly<{
24
+ durationUs: number;
25
+ sample: (timeUs: number) => HtmlOverlayAudioSample;
26
+ }>;
27
+
28
+ const LIMITS = Object.freeze({ maxSamples: 14_400_000, maxSourceBytes: 512 * 1024 * 1024,
29
+ sampleRateHz: 24_000, hopSize: 400, rateHz: 60, maxTimeUs: 3_600_000_000 });
30
+
31
+ /** One implementation validates the SDK and browser resource, before frame sampling. */
32
+ function createAudioReactivityRuntime(limits: typeof LIMITS) {
33
+ const object = (value: unknown, keys: readonly string[]): Record<string, unknown> => {
34
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
35
+ throw new TypeError("Audio reactivity requires a strict JSON object.");
36
+ }
37
+ const descriptors = Object.getOwnPropertyDescriptors(value);
38
+ if (Reflect.ownKeys(value).length !== keys.length
39
+ || keys.some(key => descriptors[key] === undefined || !("value" in descriptors[key]!))) {
40
+ throw new TypeError("Audio reactivity contains missing, unknown, or accessor fields.");
41
+ }
42
+ return value as Record<string, unknown>;
43
+ };
44
+ const integer = (value: unknown, minimum: number, maximum: number): number => {
45
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
46
+ throw new RangeError("Audio reactivity integer is outside its bound.");
47
+ }
48
+ return value;
49
+ };
50
+ const digest = (value: unknown): string => {
51
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
52
+ throw new TypeError("Audio reactivity requires a SHA-256 digest.");
53
+ }
54
+ return value;
55
+ };
56
+ const parse = (value: unknown): HtmlOverlayAudioReactivity => {
57
+ const input = object(value, ["kind", "schemaVersion", "profile", "source", "pcm", "durationUs", "rateHz", "bands"]);
58
+ if (input.kind !== "slopcamera.audio-reactivity" || input.schemaVersion !== 1
59
+ || input.profile !== "bands-v1" || input.rateHz !== limits.rateHz) {
60
+ throw new TypeError("Unsupported audio reactivity profile or version.");
61
+ }
62
+ const source = object(input.source, ["bytes", "sha256"]);
63
+ const pcm = object(input.pcm, ["sampleCount", "sha256"]);
64
+ const sampleCount = integer(pcm.sampleCount, 1, limits.maxSamples);
65
+ const durationUs = Math.round(sampleCount * 1_000_000 / limits.sampleRateHz);
66
+ if (input.durationUs !== durationUs) throw new RangeError("Audio reactivity duration differs from its PCM sample count.");
67
+ const length = Math.ceil(sampleCount / limits.hopSize) + 1;
68
+ const bands = object(input.bands, ["bass", "mid", "treble", "energy"]);
69
+ const channel = (value: unknown): readonly number[] => {
70
+ const keys = value !== null && typeof value === "object" ? Reflect.ownKeys(value) : [];
71
+ if (!Array.isArray(value) || value.length !== length || keys.length !== length + 1
72
+ || keys.some(key => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key)))) {
73
+ throw new RangeError("Audio reactivity bands must be dense arrays covering the declared duration.");
74
+ }
75
+ const copy: number[] = [];
76
+ for (let index = 0; index < length; index++) {
77
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
78
+ const sample: unknown = descriptor !== undefined && "value" in descriptor ? descriptor.value : undefined;
79
+ if (typeof sample !== "number" || !Number.isFinite(sample) || sample < 0 || sample > 1) {
80
+ throw new RangeError("Audio reactivity band values must be finite numbers in [0, 1].");
81
+ }
82
+ copy.push(sample === 0 ? 0 : sample);
83
+ }
84
+ if (copy.at(-1) !== 0) throw new RangeError("Audio reactivity requires a zero endpoint at the exact duration.");
85
+ return Object.freeze(copy);
86
+ };
87
+ return Object.freeze({
88
+ kind: "slopcamera.audio-reactivity", schemaVersion: 1, profile: "bands-v1", rateHz: 60, durationUs,
89
+ source: Object.freeze({ bytes: integer(source.bytes, 1, limits.maxSourceBytes), sha256: digest(source.sha256) }),
90
+ pcm: Object.freeze({ sampleCount, sha256: digest(pcm.sha256) }),
91
+ bands: Object.freeze({ bass: channel(bands.bass), mid: channel(bands.mid), treble: channel(bands.treble), energy: channel(bands.energy) }),
92
+ });
93
+ };
94
+ const prepareAudioReactivity = (input: unknown): PreparedHtmlOverlayAudioReactivity => {
95
+ const data = parse(input);
96
+ const zero = Object.freeze({ bass: 0, mid: 0, treble: 0, energy: 0 });
97
+ return Object.freeze({ durationUs: data.durationUs, sample(timeUs: number): HtmlOverlayAudioSample {
98
+ integer(timeUs, -limits.maxTimeUs, limits.maxTimeUs);
99
+ if (timeUs < 0 || timeUs >= data.durationUs) return zero;
100
+ const index = Math.min(data.bands.bass.length - 2, Math.floor(timeUs * limits.rateHz / 1_000_000));
101
+ const startUs = index * 1_000_000 / limits.rateHz;
102
+ const endUs = Math.min(data.durationUs, (index + 1) * 1_000_000 / limits.rateHz);
103
+ const mix = Math.max(0, Math.min(1, (timeUs - startUs) / (endUs - startUs)));
104
+ const interpolate = (band: readonly number[]) => band[index]! * (1 - mix) + band[index + 1]! * mix;
105
+ return Object.freeze({ bass: interpolate(data.bands.bass), mid: interpolate(data.bands.mid),
106
+ treble: interpolate(data.bands.treble), energy: interpolate(data.bands.energy) });
107
+ } });
108
+ };
109
+ return { parse, prepareAudioReactivity };
110
+ }
111
+
112
+ const runtime = createAudioReactivityRuntime(LIMITS);
113
+ export const HtmlOverlayAudioReactivitySchema = z.unknown().transform((input, context): HtmlOverlayAudioReactivity => {
114
+ try { return runtime.parse(input); } catch (error) {
115
+ context.addIssue({ code: "custom", message: error instanceof Error ? error.message : "Invalid audio reactivity." });
116
+ return z.NEVER;
117
+ }
118
+ });
119
+
120
+ /** Validate and copy once. Samples depend only on absolute source time, in integer microseconds. */
121
+ export function prepareHtmlOverlayAudioReactivity(input: unknown): PreparedHtmlOverlayAudioReactivity {
122
+ return runtime.prepareAudioReactivity(input);
123
+ }
124
+
125
+ export function createHtmlOverlayAudioReactivityRuntimeSource(): string {
126
+ return `(() => { const runtime = (${createAudioReactivityRuntime.toString()})(${JSON.stringify(LIMITS)}); return { prepareAudioReactivity: runtime.prepareAudioReactivity }; })()`;
127
+ }
@@ -33,6 +33,15 @@ export {
33
33
  type HtmlOverlayTiming,
34
34
  } from "./contracts";
35
35
  export { HtmlSceneInputSchema, type HtmlSceneInput } from "./scene";
36
+ export {
37
+ HTML_AUDIO_REACTIVITY_RESOURCE_NAME,
38
+ HTML_AUDIO_REACTIVITY_RESOURCE_URL_PATH,
39
+ HtmlOverlayAudioReactivitySchema,
40
+ prepareHtmlOverlayAudioReactivity,
41
+ type HtmlOverlayAudioReactivity,
42
+ type HtmlOverlayAudioSample,
43
+ type PreparedHtmlOverlayAudioReactivity,
44
+ } from "./audio-reactivity";
36
45
  export {
37
46
  HTML_OVERLAY_SCAFFOLD_CLOCK_INTEGRATIONS,
38
47
  HTML_OVERLAY_SCAFFOLD_KINDS,
@@ -11,6 +11,7 @@ import {
11
11
  import { htmlOverlayAssetLocalUrl } from "./libraries";
12
12
  import { HTML_OVERLAY_RANDOM_ALGORITHM } from "./random";
13
13
  import { createHtmlOverlayMusicClockRuntimeSource } from "./music-clock";
14
+ import { createHtmlOverlayAudioReactivityRuntimeSource } from "./audio-reactivity";
14
15
 
15
16
  const Uint32Schema = z.number().int().safe().min(0).max(0xffff_ffff);
16
17
 
@@ -713,6 +714,7 @@ export function createHtmlOverlayBrowserRuntimeSource(
713
714
  };
714
715
  },
715
716
  ...${createHtmlOverlayMusicClockRuntimeSource()},
717
+ ...${createHtmlOverlayAudioReactivityRuntimeSource()},
716
718
  parameters: deepFreeze(config.parameters),
717
719
  random,
718
720
  randomFor,
@@ -6,6 +6,8 @@ import {
6
6
  } from "./contracts";
7
7
  import { HtmlOverlayExecutionProfileSchema } from "./execution-profile";
8
8
  import { HtmlOverlayLibrarySelectionSchema } from "./libraries";
9
+ import { AUDIO_REACTIVITY_MAX_DURATION_US } from "../core/music-analysis";
10
+ import { HTML_AUDIO_REACTIVITY_RESOURCE_NAME, HTML_AUDIO_REACTIVITY_RESOURCE_URL_PATH } from "./audio-reactivity";
9
11
 
10
12
  const ResourceSchema = HtmlOverlayDeclaredResourceSchema.omit({ bytes: true, sha256: true }).extend({
11
13
  path: RepositoryRelativePathSchema,
@@ -27,8 +29,35 @@ export const HtmlSceneInputSchema = z.strictObject({
27
29
  executionProfile: HtmlOverlayExecutionProfileSchema.optional(),
28
30
  audio: z.strictObject({
29
31
  path: z.string().min(1).max(4096).refine(path => !/[\0\r\n]/u.test(path), "Audio path contains a control character."),
32
+ reactivity: z.strictObject({
33
+ profile: z.literal("bands-v1"),
34
+ resource: z.literal(HTML_AUDIO_REACTIVITY_RESOURCE_NAME).optional(),
35
+ sha256: z.string().regex(/^[a-f0-9]{64}$/u).optional(),
36
+ }).optional(),
30
37
  }).optional(),
31
38
  }).superRefine((input, context) => {
39
+ if (input.audio?.reactivity !== undefined && input.timing.durationUs > AUDIO_REACTIVITY_MAX_DURATION_US) {
40
+ context.addIssue({ code: "custom", message: "Audio reactivity is limited to ten minutes per scene." });
41
+ }
42
+ if (input.audio?.reactivity?.resource === undefined && input.audio?.reactivity !== undefined && (
43
+ input.resources.length >= 64
44
+ || input.resources.some(resource => resource.name === HTML_AUDIO_REACTIVITY_RESOURCE_NAME
45
+ || resource.urlPath === HTML_AUDIO_REACTIVITY_RESOURCE_URL_PATH)
46
+ )) {
47
+ context.addIssue({ code: "custom", message: "Audio reactivity requires one free resource slot and an unused audio-reactivity name and slopcamera/audio-reactivity.json URL path." });
48
+ }
49
+ if (input.audio?.reactivity?.resource === undefined && input.audio?.reactivity?.sha256 !== undefined) {
50
+ context.addIssue({ code: "custom", message: "An audio-reactivity sidecar digest requires its retained resource name." });
51
+ }
52
+ if (input.audio?.reactivity?.resource !== undefined && !input.resources.some(resource => (
53
+ resource.name === HTML_AUDIO_REACTIVITY_RESOURCE_NAME && resource.urlPath === HTML_AUDIO_REACTIVITY_RESOURCE_URL_PATH
54
+ && resource.mediaType === "application/json" && resource.transport === "fetch"
55
+ ))) {
56
+ context.addIssue({ code: "custom", message: "A retained audio-reactivity source must declare its fixed fetched JSON resource." });
57
+ }
58
+ if (input.audio?.reactivity?.resource !== undefined && input.audio.reactivity.sha256 === undefined) {
59
+ context.addIssue({ code: "custom", message: "A retained audio-reactivity source must record its sidecar SHA-256 digest." });
60
+ }
32
61
  const authoring = HtmlOverlayAuthoringInputSchema.safeParse({
33
62
  kind: "slopcamera.html-overlay", schemaVersion: 1, canvas: input.canvas, timing: input.timing,
34
63
  libraries: input.libraries, parameters: input.parameters, seed: input.seed,
package/dist/cli.js CHANGED
@@ -4,30 +4,25 @@ import {
4
4
  SLOPCAMERA_VERSION,
5
5
  artifactSummary,
6
6
  checkDiagramFile,
7
- desktopStatus,
8
- getLatestDesktopRelease,
9
- installDesktop,
10
- openInDesktop,
11
7
  renderDiagramFile,
12
- runMcpServer,
13
- selectDesktopAsset
14
- } from "./index-rvghzt7b.js";
8
+ runMcpServer
9
+ } from "./index-7131pg1c.js";
15
10
  import {
16
11
  installSkill,
17
12
  pathExists
18
13
  } from "./index-7308egqr.js";
19
14
  import"./index-8txs6fkn.js";
20
- import"./index-mcy8z0br.js";
15
+ import"./index-fava6pge.js";
21
16
  import {
22
17
  executeSlopcameraOperation,
23
18
  isSlopcameraOperationCode,
24
19
  searchSlopcameraOperations,
25
20
  slopcameraOperationCodes,
26
21
  withSlopcameraOperationHostAdmission
27
- } from "./index-z7239b4h.js";
22
+ } from "./index-h1k0fnjq.js";
28
23
  import {
29
24
  vectorizeImage
30
- } from "./index-p63wavx0.js";
25
+ } from "./index-zfnddgay.js";
31
26
  import {
32
27
  generateSlopcameraImageFile,
33
28
  slopcameraGatewayCredentialStatus,
@@ -41,7 +36,58 @@ import {
41
36
  // src/cli.ts
42
37
  import { writeFile } from "fs/promises";
43
38
  import { resolve } from "path";
44
- import { createInterface } from "readline/promises";
39
+
40
+ // src/support-completion.ts
41
+ function reportUsefulResult(observer) {
42
+ try {
43
+ if (observer !== undefined)
44
+ Promise.resolve(observer()).catch(() => {
45
+ return;
46
+ });
47
+ } catch {}
48
+ }
49
+
50
+ // src/support.ts
51
+ import {
52
+ maybeShowSupportInvitation,
53
+ runSupportCommand
54
+ } from "@hraness/support-foundation/node";
55
+ var supportProfile = {
56
+ id: "slopcamera",
57
+ name: "Slopcamera",
58
+ valueProposition: "Support ongoing development of local visual tools for agents.",
59
+ updates: false
60
+ };
61
+ function standaloneSupportEnvironment() {
62
+ const env = { ...process.env };
63
+ process.env.HRANESS_SUPPORT_AUDIENCE = "off";
64
+ process.env.HRANESS_SUPPORT_EMAIL = "off";
65
+ return env;
66
+ }
67
+ async function runProductSupportCommand(args, options = {}) {
68
+ const result = await runSupportCommand(supportProfile, args, {
69
+ command: ["slopcamera"],
70
+ ...options,
71
+ gitEmail: false
72
+ });
73
+ if (result.stdout !== "")
74
+ process.stdout.write(result.stdout);
75
+ if (result.stderr !== "")
76
+ process.stderr.write(result.stderr);
77
+ return result.exitCode;
78
+ }
79
+ async function showProductSupportInvitation(options = {}) {
80
+ try {
81
+ await maybeShowSupportInvitation(supportProfile, {
82
+ usefulResult: true,
83
+ command: ["slopcamera"],
84
+ ...options,
85
+ gitEmail: false
86
+ });
87
+ } catch {}
88
+ }
89
+
90
+ // src/cli.ts
45
91
  var slopcameraCliVersion = SLOPCAMERA_VERSION;
46
92
  var help = `slopcamera ${slopcameraCliVersion}
47
93
 
@@ -56,11 +102,8 @@ Usage:
56
102
  slopcamera code search [query] [--limit <number>]
57
103
  slopcamera code execute <operation> --input <JSON>
58
104
  slopcamera mcp --root <workspace>
59
- slopcamera canvas open <file.tldr|file.tldraw>
60
- slopcamera canvas status
61
- slopcamera canvas url
62
- slopcamera canvas install [--yes] [--download-only]
63
105
  slopcamera doctor
106
+ slopcamera support [--json|protocol --json|offer --json|shown <id>|release <id>|dismiss|snooze|enable|status --json]
64
107
  slopcamera skill path
65
108
  slopcamera skill install [--target codex|claude|agents] [--scope user|project] [--force]
66
109
 
@@ -71,9 +114,8 @@ Render writes the same five replaceable artifacts on every run:
71
114
  <name>.light.png
72
115
  <name>.dark.png
73
116
 
74
- The .tldr file is editable tldraw interchange. It imports into tldraw Offline,
75
- which can save the newer app-owned .tldraw bundle. Rendering does not require
76
- tldraw Offline or the tldraw SDK.
117
+ The .tldr file is editable interchange for browser-based canvas tooling.
118
+ Rendering does not require a desktop application or a bundled UI runtime.
77
119
 
78
120
  Vectorize adaptively traces a raster with a checksum-pinned VTracer binary.
79
121
  It enforces bounded input, decode, time, path, and output budgets and emits a
@@ -85,6 +127,10 @@ Set AI_GATEWAY_API_KEY, or run through \`vercel env run -- \u2026\` so
85
127
  VERCEL_OIDC_TOKEN is available. Slopcamera never stores or prints the token.
86
128
  PNG, JPEG, and WebP responses are signature-checked and published atomically.
87
129
 
130
+ Optional support: after useful work, agents can read slopcamera support protocol --json.
131
+ Discovery uses stderr without claiming an invitation; HRANESS_SUPPORT_AUDIENCE=off disables it.
132
+ No feature requires payment. Imported CLI/SDK calls and probes stay quiet.
133
+
88
134
  Code mode searches and executes a fixed semantic registry. Execute accepts
89
135
  typed JSON for one exact owned operation code; it never evaluates source text.
90
136
 
@@ -187,18 +233,6 @@ var starter = {
187
233
  ],
188
234
  edges: [{ id: "source-result", from: "source", to: "result" }]
189
235
  };
190
- async function confirmInstall() {
191
- if (!process.stdin.isTTY) {
192
- throw new Error("Pass --yes to download the 100\u2013230 MB official tldraw Offline installer");
193
- }
194
- const prompt = createInterface({ input: process.stdin, output: process.stdout });
195
- try {
196
- const answer = await prompt.question("Download, verify, and launch the official tldraw Offline installer? [y/N] ");
197
- return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
198
- } finally {
199
- prompt.close();
200
- }
201
- }
202
236
  function hostAdmissionOptions(dependencies) {
203
237
  return dependencies.hostResourceCoordinator === undefined ? {} : { hostResourceCoordinator: dependencies.hostResourceCoordinator };
204
238
  }
@@ -216,15 +250,7 @@ function canonicalArguments(args) {
216
250
  }
217
251
  throw new Error("Use slopcamera image vectorize or generate");
218
252
  }
219
- if (surface === "canvas") {
220
- if (subcommand === "open")
221
- return ["open", ...rest];
222
- if (subcommand === "status" || subcommand === "url" || subcommand === "install") {
223
- return ["desktop", subcommand, ...rest];
224
- }
225
- throw new Error("Use slopcamera canvas open, status, url, or install");
226
- }
227
- if (surface === "init" || surface === "check" || surface === "render" || surface === "vectorize" || surface === "generate" || surface === "open" || surface === "desktop") {
253
+ if (surface === "init" || surface === "check" || surface === "render" || surface === "vectorize" || surface === "generate") {
228
254
  throw new Error(`The flat \`${surface}\` command moved to a namespaced Slopcamera surface.
229
255
 
230
256
  ${help}`);
@@ -232,6 +258,10 @@ ${help}`);
232
258
  return args;
233
259
  }
234
260
  async function main(args, dependencies = {}) {
261
+ if (args[0] === "support") {
262
+ process.exitCode = await runProductSupportCommand(args.slice(1), dependencies.supportEnvironment === undefined ? {} : { env: dependencies.supportEnvironment });
263
+ return;
264
+ }
235
265
  const [command, ...rest] = canonicalArguments(args);
236
266
  if (command === undefined || command === "help" || command === "--help" || command === "-h") {
237
267
  console.log(help);
@@ -249,6 +279,7 @@ async function main(args, dependencies = {}) {
249
279
  await writeFile(filePath, `${JSON.stringify(starter, null, 2)}
250
280
  `);
251
281
  console.log(`Created ${filePath}`);
282
+ reportUsefulResult(dependencies.onUsefulResult);
252
283
  return;
253
284
  }
254
285
  if (command === "check") {
@@ -274,6 +305,7 @@ async function main(args, dependencies = {}) {
274
305
  }), hostAdmissionOptions(dependencies));
275
306
  console.log(artifactSummary(result.artifacts));
276
307
  printFindings(result.findings);
308
+ reportUsefulResult(dependencies.onUsefulResult);
277
309
  return;
278
310
  }
279
311
  if (command === "vectorize") {
@@ -304,6 +336,7 @@ async function main(args, dependencies = {}) {
304
336
  } else {
305
337
  (dependencies.log ?? console.log)(`Vectorized ${result.receipt.width}\xD7${result.receipt.height} with ` + `${result.receipt.profile}/${result.receipt.representation}: ${result.outputPath}`);
306
338
  }
339
+ reportUsefulResult(dependencies.onUsefulResult);
307
340
  return;
308
341
  }
309
342
  if (command === "generate") {
@@ -329,6 +362,7 @@ async function main(args, dependencies = {}) {
329
362
  } else {
330
363
  (dependencies.log ?? console.log)(`Generated ${result.mediaType} with ${result.model}: ${result.outputPath} (${result.bytes} bytes, request ${result.requestId})`);
331
364
  }
365
+ reportUsefulResult(dependencies.onUsefulResult);
332
366
  return;
333
367
  }
334
368
  if (command === "code") {
@@ -367,6 +401,9 @@ async function main(args, dependencies = {}) {
367
401
  ...hostAdmissionOptions(dependencies)
368
402
  });
369
403
  (dependencies.log ?? console.log)(JSON.stringify({ operation, result }, null, 2));
404
+ if (operation === "slopcamera.diagram.render" || operation === "slopcamera.image.vectorize" || operation === "slopcamera.image.generate") {
405
+ reportUsefulResult(dependencies.onUsefulResult);
406
+ }
370
407
  return;
371
408
  }
372
409
  throw new Error("Use slopcamera code search [query] or slopcamera code execute <operation> --input <JSON>");
@@ -382,14 +419,7 @@ async function main(args, dependencies = {}) {
382
419
  });
383
420
  return;
384
421
  }
385
- if (command === "open") {
386
- const parsed = parseArguments(rest, new Set);
387
- await openInDesktop(requiredPositional(parsed, 0, "tldraw file"));
388
- console.log("Opened in tldraw Offline.");
389
- return;
390
- }
391
422
  if (command === "doctor") {
392
- const status = await desktopStatus();
393
423
  console.log(`slopcamera ${slopcameraCliVersion}`);
394
424
  console.log(`Bun ${process.versions.bun ?? "not detected"}`);
395
425
  console.log("Headless diagram SVG/PNG/tldraw renderer ready");
@@ -397,42 +427,8 @@ async function main(args, dependencies = {}) {
397
427
  console.log("Root-relative MCP check/render server ready (trusted local workspace)");
398
428
  const gateway = slopcameraGatewayCredentialStatus();
399
429
  console.log(gateway.available ? `Vercel AI Gateway ready via ${gateway.source}` : "Vercel AI Gateway requires AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN");
400
- console.log(status.installedPath === null ? "tldraw Offline not installed (optional)" : `tldraw Offline: ${status.installedPath}`);
401
- console.log(status.server === null ? "tldraw Offline agent server not running (optional)" : `tldraw Offline agent server: localhost:${status.server.port}`);
402
430
  return;
403
431
  }
404
- if (command === "desktop") {
405
- const [subcommand, ...subcommandArgs] = rest;
406
- if (subcommand === "status") {
407
- const status = await desktopStatus();
408
- console.log(JSON.stringify(status, null, 2));
409
- return;
410
- }
411
- if (subcommand === "url") {
412
- const release = await getLatestDesktopRelease();
413
- const asset = selectDesktopAsset(release);
414
- console.log(JSON.stringify({
415
- release: release.tag_name,
416
- releaseUrl: release.html_url,
417
- asset: asset.name,
418
- url: asset.browser_download_url,
419
- bytes: asset.size,
420
- sha256: asset.digest
421
- }, null, 2));
422
- return;
423
- }
424
- if (subcommand === "install") {
425
- const parsed = parseArguments(subcommandArgs, new Set);
426
- if (!parsed.flags.has("yes") && !await confirmInstall()) {
427
- console.log("Cancelled.");
428
- return;
429
- }
430
- const result = await installDesktop({ downloadOnly: parsed.flags.has("download-only") });
431
- console.log(`${parsed.flags.has("download-only") ? "Downloaded" : "Prepared"} tldraw Offline ${result.release}: ${result.filePath}`);
432
- return;
433
- }
434
- throw new Error("Use slopcamera canvas status, url, or install");
435
- }
436
432
  if (command === "skill") {
437
433
  const [subcommand, ...subcommandArgs] = rest;
438
434
  if (subcommand === "path") {
@@ -467,7 +463,13 @@ ${help}`);
467
463
  }
468
464
  if (import.meta.main) {
469
465
  try {
470
- await main(process.argv.slice(2));
466
+ const env = standaloneSupportEnvironment();
467
+ let usefulResult = false;
468
+ await main(process.argv.slice(2), { supportEnvironment: env, onUsefulResult: () => {
469
+ usefulResult = true;
470
+ } });
471
+ if (usefulResult && (process.exitCode ?? 0) === 0)
472
+ await showProductSupportInvitation({ env });
471
473
  } catch (error) {
472
474
  console.error(error instanceof Error ? error.message : String(error));
473
475
  process.exitCode = 1;