@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.
- package/NOTICE.md +1 -7
- package/PRIVACY.md +16 -7
- package/README.md +37 -12
- package/apps/desktop/README.md +14 -130
- package/apps/desktop/application/context.ts +0 -9
- package/apps/desktop/application/default-registry.ts +0 -10
- package/apps/desktop/application/operation.ts +1 -5
- package/apps/desktop/application/operations/index.ts +0 -1
- package/apps/desktop/cli/args.ts +14 -86
- package/apps/desktop/cli/command-host-resources.ts +2 -1
- package/apps/desktop/cli/commands.ts +28 -67
- package/apps/desktop/cli/help.ts +12 -18
- package/apps/desktop/cli/html-scene.ts +139 -28
- package/apps/desktop/cli/io.ts +2 -1
- package/apps/desktop/cli/main.ts +23 -47
- package/apps/desktop/cli/menubar.ts +244 -0
- package/apps/desktop/cli/music-analysis-service.ts +61 -0
- package/apps/desktop/cli/portable-surface.ts +12 -4
- package/apps/desktop/code/application-node-planner.ts +0 -47
- package/apps/desktop/code/host-source-layout.ts +46 -0
- package/apps/desktop/code/public.ts +0 -1
- package/apps/desktop/code/runtime-identity.ts +50 -8
- package/apps/desktop/code/semantic-builder.ts +0 -43
- package/apps/desktop/code/source-bundle.ts +23 -6
- package/apps/desktop/code/source-typecheck.ts +131 -43
- package/apps/desktop/code/worker-client.ts +14 -2
- package/apps/desktop/core/music-analysis.ts +124 -1
- package/apps/desktop/dist/cli/main.js +244 -250
- package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
- package/apps/desktop/html-overlay/index.ts +9 -0
- package/apps/desktop/html-overlay/runtime.ts +2 -0
- package/apps/desktop/html-overlay/scene.ts +29 -0
- package/dist/cli.js +83 -81
- package/dist/{index-rvghzt7b.js → index-7131pg1c.js} +43 -236
- package/dist/{index-mcy8z0br.js → index-fava6pge.js} +1 -1
- package/dist/{index-z7239b4h.js → index-h1k0fnjq.js} +1 -1
- package/dist/{index-p63wavx0.js → index-zfnddgay.js} +1 -1
- package/dist/index.js +4 -18
- package/dist/operations.js +2 -2
- package/dist/vectorize/worker.js +1 -1
- package/dist/workflow.js +3 -3
- package/docs/README.md +1 -1
- package/docs/studio.md +1 -1
- package/package.json +6 -21
- package/skills/slopcamera/SKILL.md +8 -0
- package/skills/slopcamera/references/diagrams.md +5 -14
- package/skills/slopcamera/references/install.md +1 -1
- package/skills/slopcamera/references/music-video.md +23 -3
- package/skills/slopcamera/references/reference-led-3d.md +1 -1
- package/skills/slopcamera/references/support.md +25 -0
- package/skills/slopcamera/references/web-media-excerpts.md +120 -0
- package/src/cli.ts +28 -98
- package/src/index.ts +0 -23
- package/src/support-completion.ts +16 -0
- package/src/support.ts +42 -0
- package/src/version.ts +1 -1
- package/apps/desktop/application/operations/recording/index.ts +0 -5
- package/apps/desktop/application/operations/recording/pause.ts +0 -34
- package/apps/desktop/application/operations/recording/resume.ts +0 -34
- package/apps/desktop/application/operations/recording/shared.ts +0 -199
- package/apps/desktop/application/operations/recording/start.ts +0 -169
- package/apps/desktop/application/operations/recording/stop.ts +0 -34
- package/apps/desktop/capture/protocol.ts +0 -689
- package/apps/desktop/cli/capture-bundle.ts +0 -1117
- package/apps/desktop/cli/recording-controller.ts +0 -1567
- package/apps/desktop/cli/recording-daemon.ts +0 -931
- 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
|
-
|
|
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-
|
|
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-
|
|
22
|
+
} from "./index-h1k0fnjq.js";
|
|
28
23
|
import {
|
|
29
24
|
vectorizeImage
|
|
30
|
-
} from "./index-
|
|
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
|
-
|
|
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
|
|
75
|
-
|
|
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 === "
|
|
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
|
-
|
|
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;
|