@hraness/slopcamera 3.7.0 → 3.8.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.
- package/README.md +10 -9
- package/apps/desktop/README.md +2 -9
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/application/spatial-assets.ts +1 -1
- package/apps/desktop/cli/{menubar-status.ts → activity-status.ts} +23 -21
- package/apps/desktop/cli/args.ts +2 -10
- package/apps/desktop/cli/command-host-resources.ts +0 -1
- package/apps/desktop/cli/commands.ts +14 -9
- package/apps/desktop/cli/desktop-status.ts +271 -0
- package/apps/desktop/cli/help.ts +10 -24
- package/apps/desktop/cli/legacy-login.ts +184 -0
- package/apps/desktop/cli/main.ts +8 -0
- package/apps/desktop/cli/outputs.ts +19 -0
- package/apps/desktop/cli/registry.ts +314 -0
- package/apps/desktop/cli/verbs.ts +193 -0
- package/apps/desktop/dist/cli/main.js +293 -306
- package/dist/cli.js +1 -1
- package/dist/{index-nv3byqa1.js → index-k9bjw80z.js} +1 -1
- package/dist/index.js +1 -1
- package/docs/README.md +1 -1
- package/package.json +12 -6
- package/skills/slopcamera/SKILL.md +2 -2
- package/skills/slopcamera/references/launch-films.md +4 -0
- package/skills/slopcamera/references/reference-led-3d.md +1 -1
- package/src/version.ts +1 -1
- package/apps/desktop/cli/menubar.ts +0 -272
package/README.md
CHANGED
|
@@ -103,15 +103,16 @@ Native engines install separately. The [source-install guide](docs/how-to/use-cu
|
|
|
103
103
|
explains workspace placement and durable-run identity; the
|
|
104
104
|
[capability reference](docs/reference/capabilities.md) names runtime requirements.
|
|
105
105
|
|
|
106
|
-
SlopCamera is a CLI
|
|
107
|
-
what
|
|
108
|
-
balance
|
|
109
|
-
companion
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
106
|
+
SlopCamera is a CLI with no menu-bar app or background process.
|
|
107
|
+
`slopcamera status` shows what is running, how the last job ended, your last
|
|
108
|
+
known credits balance, the newest outputs and whether the retired menu-bar
|
|
109
|
+
companion still opens at login. `slopcamera tui` keeps
|
|
110
|
+
that screen open and refreshes it; `slopcamera tui --snapshot` prints it once
|
|
111
|
+
and `--json` returns the data. `slopcamera outputs list|open|reveal` lists the
|
|
112
|
+
newest outputs or opens one on this Mac, `slopcamera commands --json` lists
|
|
113
|
+
every command with what it may change, and `slopcamera legacy retire` stops
|
|
114
|
+
the retired menu-bar companion opening at login by moving its LaunchAgent aside; it
|
|
115
|
+
deletes nothing, and `slopcamera doctor` prints the command that restores it.
|
|
115
116
|
|
|
116
117
|
<details>
|
|
117
118
|
<summary>Historical Atet release evidence</summary>
|
package/apps/desktop/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SlopCamera local host and CLI
|
|
2
2
|
|
|
3
|
-
Slopcamera reads finished recording bundles from repository-local directories, combines independent camera and audio takes on one typed project clock, then applies non-destructive edits through the `slopcamera` CLI. The CLI is the product surface for agents.
|
|
3
|
+
Slopcamera reads finished recording bundles from repository-local directories, combines independent camera and audio takes on one typed project clock, then applies non-destructive edits through the `slopcamera` CLI. The CLI is the product surface for agents. `slopcamera status` and `slopcamera tui` show what is running, credits and the newest agent outputs in the terminal.
|
|
4
4
|
|
|
5
5
|
## Recording bundles
|
|
6
6
|
|
|
@@ -819,13 +819,6 @@ bun run test:html-overlay
|
|
|
819
819
|
bun run benchmark:code-concurrency
|
|
820
820
|
```
|
|
821
821
|
|
|
822
|
-
The optional Rust menu-bar companion lives under `desktop/` at the repository root and builds with Cargo:
|
|
823
|
-
|
|
824
|
-
```sh
|
|
825
|
-
cargo build --manifest-path desktop/menubar/Cargo.toml
|
|
826
|
-
slopcamera menubar
|
|
827
|
-
```
|
|
828
|
-
|
|
829
822
|
`test:html-overlay` is portable and leaves the real-browser tests registered as
|
|
830
823
|
skipped. `test:html-overlay:browser:macos` runs the real Chrome frame suite,
|
|
831
824
|
including a declared PNG. `test:html-overlay:libraries:macos` additionally
|
|
@@ -847,4 +840,4 @@ The compiled smoke copies the executable away from the checkout and proves
|
|
|
847
840
|
diagram initialization/rendering plus PNG-to-SVG vectorization without
|
|
848
841
|
`node_modules`.
|
|
849
842
|
|
|
850
|
-
Screen recording, microphone, camera, Input Monitoring, and Accessibility are separate macOS permissions. A missing optional source becomes a typed diagnostic; strict-input mode fails before recording begins. Slopcamera ships as a CLI
|
|
843
|
+
Screen recording, microphone, camera, Input Monitoring, and Accessibility are separate macOS permissions. A missing optional source becomes a typed diagnostic; strict-input mode fails before recording begins. Slopcamera ships as a CLI; it does not produce, install, sign, or notarize a desktop application.
|
|
@@ -4,7 +4,7 @@ import type { z } from "zod";
|
|
|
4
4
|
|
|
5
5
|
import type { ApplicationContext } from "./context";
|
|
6
6
|
|
|
7
|
-
export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.
|
|
7
|
+
export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.8.0" as const;
|
|
8
8
|
|
|
9
9
|
export const OPERATION_KINDS = [
|
|
10
10
|
"scene.audit",
|
|
@@ -459,7 +459,7 @@ export async function withPreparedSpatialAssets<Result>(
|
|
|
459
459
|
}
|
|
460
460
|
const decoded = await raster(bytes, interpretation.alpha === "opaque");
|
|
461
461
|
if (decoded.width !== interpretation.width || decoded.height !== interpretation.height) throw new RangeError("Image dimensions differ from its interpretation.");
|
|
462
|
-
profiles.add("sdr-png-jpeg-sharp-0.35.
|
|
462
|
+
profiles.add("sdr-png-jpeg-sharp-0.35.4");
|
|
463
463
|
return publishRaster(decoded);
|
|
464
464
|
};
|
|
465
465
|
const native = async (argv: readonly [string, ...string[]], maxOutputBytes = 16_384) => {
|
|
@@ -5,18 +5,20 @@ import { join } from "node:path";
|
|
|
5
5
|
import type { CliCommand } from "./args";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* The small status file
|
|
8
|
+
* The small status file `slopcamera status` and `slopcamera tui` read: what Slopcamera is doing now,
|
|
9
9
|
* how the last render ended, and the last credits balance a command saw.
|
|
10
10
|
* It holds plain labels, times and fixed error codes only — never paths,
|
|
11
11
|
* prompts, tokens or account identifiers. Writes are best effort: a status
|
|
12
12
|
* that can't be saved never fails the command.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
// The on-disk name predates the menu bar's retirement and stays frozen, so a
|
|
15
|
+
// rolled-back release and existing state keep reading the same file.
|
|
16
|
+
export const ACTIVITY_STATUS_FILE = "menubar-status.json";
|
|
15
17
|
const MAX_STATUS_BYTES = 16 * 1024;
|
|
16
18
|
|
|
17
19
|
export type ActivityState = "running" | "done" | "failed";
|
|
18
20
|
|
|
19
|
-
export interface
|
|
21
|
+
export interface RecordedActivity {
|
|
20
22
|
readonly state: ActivityState;
|
|
21
23
|
readonly label: string;
|
|
22
24
|
readonly startedAt: number;
|
|
@@ -24,16 +26,16 @@ export interface MenubarActivity {
|
|
|
24
26
|
readonly error?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export interface
|
|
29
|
+
export interface RecordedCredits {
|
|
28
30
|
readonly usd: string;
|
|
29
31
|
readonly low: boolean;
|
|
30
32
|
readonly checkedAt: number;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
export interface
|
|
35
|
+
export interface ActivityStatus {
|
|
34
36
|
readonly schemaVersion: 1;
|
|
35
|
-
readonly activity?:
|
|
36
|
-
readonly credits?:
|
|
37
|
+
readonly activity?: RecordedActivity;
|
|
38
|
+
readonly credits?: RecordedCredits;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/** Plain labels for the commands that make media. Everything else leaves the status alone. */
|
|
@@ -58,13 +60,13 @@ export function activityLabel(command: CliCommand): string | undefined {
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
function parse(text: string):
|
|
63
|
+
function parse(text: string): ActivityStatus {
|
|
62
64
|
try {
|
|
63
65
|
const value: unknown = JSON.parse(text);
|
|
64
66
|
if (typeof value !== "object" || value === null || (value as { schemaVersion?: unknown }).schemaVersion !== 1) return { schemaVersion: 1 };
|
|
65
67
|
const record = value as Record<string, unknown>;
|
|
66
|
-
const activity = record.activity as
|
|
67
|
-
const credits = record.credits as
|
|
68
|
+
const activity = record.activity as RecordedActivity | undefined;
|
|
69
|
+
const credits = record.credits as RecordedCredits | undefined;
|
|
68
70
|
return {
|
|
69
71
|
schemaVersion: 1,
|
|
70
72
|
...(activity !== undefined && typeof activity === "object" && typeof activity.label === "string" ? { activity } : {}),
|
|
@@ -75,8 +77,8 @@ function parse(text: string): MenubarStatus {
|
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
export function
|
|
79
|
-
const path = join(stateRoot,
|
|
80
|
+
export function readActivityStatus(stateRoot: string): ActivityStatus {
|
|
81
|
+
const path = join(stateRoot, ACTIVITY_STATUS_FILE);
|
|
80
82
|
try {
|
|
81
83
|
const info = lstatSync(path);
|
|
82
84
|
if (!info.isFile() || info.size > MAX_STATUS_BYTES) return { schemaVersion: 1 };
|
|
@@ -86,7 +88,7 @@ export function readMenubarStatus(stateRoot: string): MenubarStatus {
|
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
const LOCK_FILE = `.${
|
|
91
|
+
const LOCK_FILE = `.${ACTIVITY_STATUS_FILE}.lock`;
|
|
90
92
|
/** A held lock blocks a write for this long before it is skipped. */
|
|
91
93
|
const LOCK_BUDGET_MS = 250;
|
|
92
94
|
/** A lock older than this is a crashed writer's and is reclaimed. */
|
|
@@ -128,14 +130,14 @@ function withStatusLock<T>(stateRoot: string, update: () => T): T | undefined {
|
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
/** Replaces the status file atomically with an owner-only file. Never throws. */
|
|
131
|
-
export function
|
|
133
|
+
export function writeActivityStatus(stateRoot: string, update: (current: ActivityStatus) => ActivityStatus): void {
|
|
132
134
|
withStatusLock(stateRoot, () => {
|
|
133
|
-
const path = join(stateRoot,
|
|
134
|
-
const staged = join(stateRoot, `.${
|
|
135
|
+
const path = join(stateRoot, ACTIVITY_STATUS_FILE);
|
|
136
|
+
const staged = join(stateRoot, `.${ACTIVITY_STATUS_FILE}.${randomUUID()}.tmp`);
|
|
135
137
|
try {
|
|
136
138
|
const root = lstatSync(stateRoot);
|
|
137
139
|
if (!root.isDirectory()) return;
|
|
138
|
-
const text = `${JSON.stringify(update(
|
|
140
|
+
const text = `${JSON.stringify(update(readActivityStatus(stateRoot)))}\n`;
|
|
139
141
|
if (Buffer.byteLength(text) > MAX_STATUS_BYTES) return;
|
|
140
142
|
const fd = openSync(staged, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
141
143
|
try { writeSync(fd, text); } finally { closeSync(fd); }
|
|
@@ -147,10 +149,10 @@ export function writeMenubarStatus(stateRoot: string, update: (current: MenubarS
|
|
|
147
149
|
});
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
export function recordActivity(stateRoot: string, activity:
|
|
151
|
-
|
|
152
|
+
export function recordActivity(stateRoot: string, activity: RecordedActivity): void {
|
|
153
|
+
writeActivityStatus(stateRoot, (current) => ({ ...current, activity }));
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
export function recordCredits(stateRoot: string, credits:
|
|
155
|
-
|
|
156
|
+
export function recordCredits(stateRoot: string, credits: RecordedCredits): void {
|
|
157
|
+
writeActivityStatus(stateRoot, (current) => ({ ...current, credits }));
|
|
156
158
|
}
|
package/apps/desktop/cli/args.ts
CHANGED
|
@@ -335,7 +335,6 @@ export type CliCommand =
|
|
|
335
335
|
readonly tint: number | undefined;
|
|
336
336
|
readonly videoStreamIndex: number;
|
|
337
337
|
} & JsonOption)
|
|
338
|
-
| ({ readonly kind: "menubar"; readonly action: "run" | "install" | "uninstall" | "status"; readonly mode: "foreground" | "background" } & JsonOption)
|
|
339
338
|
| ({ readonly kind: "outputs" } & JsonOption)
|
|
340
339
|
| ({ readonly kind: "credits-status" } & JsonOption)
|
|
341
340
|
| ({
|
|
@@ -3716,15 +3715,8 @@ export function parseCliArgs(argv: readonly string[]): CliCommand {
|
|
|
3716
3715
|
case "project": return parseProject(argv.slice(1));
|
|
3717
3716
|
case "inspect": return parseInspect(argv.slice(1));
|
|
3718
3717
|
case "events": return parseEvents(argv.slice(1));
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
const positionals = parsed.positionals;
|
|
3722
|
-
if (positionals.length > 1 || (positionals[0] !== undefined && !["install", "uninstall", "status", "start"].includes(positionals[0]))) throw new CliError("usage", "Use slopcamera menubar [--foreground|--background] or slopcamera menubar install|uninstall|status|start. See slopcamera help menubar.");
|
|
3723
|
-
if (positionals[0] !== undefined && (optionFlag(parsed, "--foreground") || optionFlag(parsed, "--background"))) throw new CliError("usage", "Choose a menu-bar action or a run mode, not both.");
|
|
3724
|
-
// `start` opens it now and returns, like --background; the login item's hints use it.
|
|
3725
|
-
if (positionals[0] === "start") return { kind: "menubar", action: "run", mode: "background", json: optionFlag(parsed, "--json") };
|
|
3726
|
-
return { kind: "menubar", action: (positionals[0] as "install" | "uninstall" | "status" | undefined) ?? "run", mode: optionFlag(parsed, "--background") ? "background" : "foreground", json: optionFlag(parsed, "--json") };
|
|
3727
|
-
}
|
|
3718
|
+
// Retired with the menu-bar companion; the old command name explains where it went.
|
|
3719
|
+
case "menubar": throw new CliError("usage", "The menu bar was retired. Use slopcamera status or slopcamera tui to see what Slopcamera is doing, and slopcamera legacy retire to stop an old copy opening at login.");
|
|
3728
3720
|
case "outputs": {
|
|
3729
3721
|
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
3730
3722
|
exactPositionals(parsed, 0, "slopcamera outputs [--json]");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { activityLabel, recordActivity, recordCredits } from "./
|
|
1
|
+
import { activityLabel, recordActivity, recordCredits } from "./activity-status";
|
|
2
2
|
import { reportUsefulResult, type UsefulResultObserver } from "../../../src/support-completion";
|
|
3
3
|
import { SlopcameraCloudError } from "../../../src/cloud-errors";
|
|
4
4
|
import {
|
|
@@ -190,7 +190,8 @@ import { PlaywrightHtmlOverlayRenderer } from "./html-overlay-renderer";
|
|
|
190
190
|
import { executeHtmlSceneCommand } from "./html-scene";
|
|
191
191
|
import { executeHtmlFilmCommand, formatHtmlFilmResult } from "./html-film";
|
|
192
192
|
import { BunProcessRunner, processIo, writeJson, writeLine, type CliIo, type ProcessRunner } from "./io";
|
|
193
|
-
import {
|
|
193
|
+
import { reportOutputsRoot } from "./outputs";
|
|
194
|
+
import { inspectLegacyLogin, retiredLegacyLogins } from "./legacy-login";
|
|
194
195
|
import {
|
|
195
196
|
codePreparationHostResourceClaims,
|
|
196
197
|
combineHostResourceClaims,
|
|
@@ -364,7 +365,7 @@ import {
|
|
|
364
365
|
workflowRunStore,
|
|
365
366
|
} from "./workflow-runs";
|
|
366
367
|
|
|
367
|
-
export const SLOPCAMERA_VERSION = "3.
|
|
368
|
+
export const SLOPCAMERA_VERSION = "3.8.0";
|
|
368
369
|
|
|
369
370
|
// Legacy direct renders predate per-target output contracts. Keep them
|
|
370
371
|
// bounded generously enough for long-form production while preventing one
|
|
@@ -7984,6 +7985,10 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
7984
7985
|
platform: { architecture: process.arch, macOSVersion: macVersion.stdout.trim(), name: context.io.platform },
|
|
7985
7986
|
repositoryRoot: context.paths.repositoryRoot,
|
|
7986
7987
|
tools: Object.fromEntries(capabilities.map((capability) => [capability.name, capability])),
|
|
7988
|
+
legacyLoginItem: {
|
|
7989
|
+
...inspectLegacyLogin(context.io.env.HOME),
|
|
7990
|
+
retired: retiredLegacyLogins(context.io.env.HOME),
|
|
7991
|
+
},
|
|
7987
7992
|
version: context.version,
|
|
7988
7993
|
};
|
|
7989
7994
|
writeValue(context.io, command.json, output, () => [
|
|
@@ -7992,6 +7997,12 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
7992
7997
|
`platform ${output.platform.name} ${output.platform.macOSVersion}`,
|
|
7993
7998
|
...capabilities.map((item) => `${item.name} ${item.available ? item.version ?? item.command : "unavailable"}`),
|
|
7994
7999
|
`emoji ${emoji.provenance} ${emoji.installedCount}/${emoji.catalogCount}; generate: ${emoji.generationCommand}`,
|
|
8000
|
+
output.legacyLoginItem.state === "found"
|
|
8001
|
+
? "login the old menu bar still opens at login; run: slopcamera legacy retire"
|
|
8002
|
+
: output.legacyLoginItem.state === "not-ours"
|
|
8003
|
+
? `login ${output.legacyLoginItem.label ?? "an item"} was changed outside Slopcamera and is left alone`
|
|
8004
|
+
: "login nothing starts at login",
|
|
8005
|
+
...output.legacyLoginItem.retired.map((item) => `retired ${item.path}; restore: ${item.restore}`),
|
|
7995
8006
|
].join("\n"));
|
|
7996
8007
|
return;
|
|
7997
8008
|
}
|
|
@@ -8032,11 +8043,6 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
8032
8043
|
case "credits-forget": await handleCreditsForget(context, command); return;
|
|
8033
8044
|
case "media-audio": await handleMediaAudio(context, command); return;
|
|
8034
8045
|
case "media-color": await handleMediaColor(context, command); return;
|
|
8035
|
-
case "menubar": {
|
|
8036
|
-
if (command.action === "run") await launchMenubar(context.io, context.paths.repositoryRoot, command.json, command.mode);
|
|
8037
|
-
else await manageMenubar(context.io, context.paths.repositoryRoot, command.action, command.json);
|
|
8038
|
-
return;
|
|
8039
|
-
}
|
|
8040
8046
|
case "outputs": {
|
|
8041
8047
|
await reportOutputsRoot(context.io, context.stateRoot, command.json);
|
|
8042
8048
|
return;
|
|
@@ -8365,7 +8371,6 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
|
|
|
8365
8371
|
case "fillers-list":
|
|
8366
8372
|
case "emoji-search":
|
|
8367
8373
|
case "emoji-resolve":
|
|
8368
|
-
case "menubar":
|
|
8369
8374
|
case "outputs":
|
|
8370
8375
|
case "complete": return undefined;
|
|
8371
8376
|
// Fresh projects write outside an existing mutable bundle and publish by
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { lstatSync, readdirSync } from "node:fs";
|
|
2
|
+
import { extname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { okEnvelope, type Envelope, type NextStep } from "@hraness/desktop-foundation/registry";
|
|
5
|
+
import { box, clean, table, type View } from "@hraness/desktop-foundation/tui";
|
|
6
|
+
|
|
7
|
+
import type { LegacyLoginReport } from "./legacy-login";
|
|
8
|
+
import type { ActivityStatus } from "./activity-status";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `slopcamera status` and `slopcamera tui`: what Slopcamera is doing, how
|
|
12
|
+
* the last job ended, the last credits balance a command saw, the newest
|
|
13
|
+
* outputs, and whether the retired menu bar still starts at login. Pure
|
|
14
|
+
* except `listOutputs`, so every state has a golden.
|
|
15
|
+
*/
|
|
16
|
+
export const STATUS_SCHEMA = "slopcamera.status/1";
|
|
17
|
+
export const OUTPUTS_LIMIT = 3;
|
|
18
|
+
/** A job still marked running after this long crashed or was killed. */
|
|
19
|
+
export const STALE_RUNNING_MS = 6 * 60 * 60 * 1000;
|
|
20
|
+
const MAX_LABEL_CHARS = 40;
|
|
21
|
+
|
|
22
|
+
export type ActivityState = "idle" | "running" | "done" | "failed" | "stale";
|
|
23
|
+
|
|
24
|
+
export interface StatusActivity {
|
|
25
|
+
readonly state: ActivityState;
|
|
26
|
+
readonly headline: string;
|
|
27
|
+
readonly detail: string;
|
|
28
|
+
readonly label?: string;
|
|
29
|
+
readonly startedAt?: string;
|
|
30
|
+
readonly finishedAt?: string;
|
|
31
|
+
readonly error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StatusCredits {
|
|
35
|
+
readonly usd: string;
|
|
36
|
+
readonly low: boolean;
|
|
37
|
+
readonly checkedAt: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface StatusOutput {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly kind: string;
|
|
43
|
+
readonly bytes: number;
|
|
44
|
+
readonly modifiedAt: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface StatusOutputs {
|
|
48
|
+
readonly root: string;
|
|
49
|
+
readonly total: number;
|
|
50
|
+
readonly latest: readonly StatusOutput[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface StatusData {
|
|
54
|
+
readonly product: "slopcamera";
|
|
55
|
+
readonly version: string;
|
|
56
|
+
readonly attention: boolean;
|
|
57
|
+
readonly activity: StatusActivity;
|
|
58
|
+
readonly credits: StatusCredits | null;
|
|
59
|
+
readonly outputs: StatusOutputs;
|
|
60
|
+
readonly legacyLoginItem: LegacyLoginReport;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface StatusInput {
|
|
64
|
+
readonly version: string;
|
|
65
|
+
readonly status: ActivityStatus;
|
|
66
|
+
readonly outputs: StatusOutputs;
|
|
67
|
+
readonly legacyLogin: LegacyLoginReport;
|
|
68
|
+
readonly now: Date;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function ago(thenMs: number, nowMs: number): string {
|
|
72
|
+
const seconds = Math.max(0, Math.floor((nowMs - thenMs) / 1000));
|
|
73
|
+
if (seconds < 60) return "just now";
|
|
74
|
+
if (seconds < 3_600) return `${Math.floor(seconds / 60)} min ago`;
|
|
75
|
+
if (seconds < 7_200) return "1 hour ago";
|
|
76
|
+
if (seconds < 86_400) return `${Math.floor(seconds / 3_600)} hours ago`;
|
|
77
|
+
if (seconds < 172_800) return "yesterday";
|
|
78
|
+
return `${Math.floor(seconds / 86_400)} days ago`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function dollars(usd: string): string {
|
|
82
|
+
const bare = usd.replace(/^\$+/u, "");
|
|
83
|
+
return bare !== "" && /^[0-9.,]+$/u.test(bare) ? `$${bare}` : usd;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function explain(code: string): string {
|
|
87
|
+
switch (code) {
|
|
88
|
+
case "cancelled": return "It was stopped before it finished";
|
|
89
|
+
case "not-found": return "A file it needed wasn't found";
|
|
90
|
+
case "unavailable": return "A tool it needed isn't available on this computer";
|
|
91
|
+
case "authorization-required": return "It needs more credits first";
|
|
92
|
+
case "subprocess": return "A render tool stopped with an error";
|
|
93
|
+
case "invalid-data":
|
|
94
|
+
case "incompatible": return "Its input wasn't in a form Slopcamera can use";
|
|
95
|
+
case "unsafe-path": return "It was asked to use a folder Slopcamera won't write to";
|
|
96
|
+
case "usage": return "The command had a typo or a missing option";
|
|
97
|
+
case "conflict": return "Something else was changing the same project";
|
|
98
|
+
default: return "Something went wrong. Run the command again to see why";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function plain(text: string, max: number): boolean {
|
|
103
|
+
return text !== "" && [...text].length <= max && /^[\p{L}\p{N} .,'$-]+$/u.test(text);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function iso(ms: number): string | undefined {
|
|
107
|
+
return Number.isFinite(ms) && ms >= 0 && ms <= 8.64e15 ? new Date(ms).toISOString() : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function activityOf(status: ActivityStatus, now: number): StatusActivity {
|
|
111
|
+
const activity = status.activity;
|
|
112
|
+
if (activity === undefined || !plain(activity.label, MAX_LABEL_CHARS) || !Number.isFinite(activity.startedAt)) {
|
|
113
|
+
return { state: "idle", headline: "Ready", detail: "Nothing rendered yet" };
|
|
114
|
+
}
|
|
115
|
+
const startedAt = iso(activity.startedAt);
|
|
116
|
+
const finishedMs = activity.finishedAt ?? activity.startedAt;
|
|
117
|
+
const finishedAt = activity.finishedAt === undefined ? undefined : iso(activity.finishedAt);
|
|
118
|
+
const times = { ...(startedAt === undefined ? {} : { startedAt }), ...(finishedAt === undefined ? {} : { finishedAt }) };
|
|
119
|
+
if (activity.state === "running") {
|
|
120
|
+
if (now - activity.startedAt < STALE_RUNNING_MS) {
|
|
121
|
+
return { state: "running", headline: activity.label, detail: `Started ${ago(activity.startedAt, now)}`, label: activity.label, ...times };
|
|
122
|
+
}
|
|
123
|
+
return { state: "stale", headline: "Last job stopped early", detail: `${activity.label} · started ${ago(activity.startedAt, now)}`, label: activity.label, ...times };
|
|
124
|
+
}
|
|
125
|
+
if (activity.state === "failed") {
|
|
126
|
+
const error = typeof activity.error === "string" && /^[a-z][a-z-]{0,40}$/u.test(activity.error) ? activity.error : "internal";
|
|
127
|
+
return { state: "failed", headline: "Last job didn't finish", detail: `${activity.label} · ${explain(error)}`, label: activity.label, error, ...times };
|
|
128
|
+
}
|
|
129
|
+
return { state: "done", headline: "Ready", detail: `Last job finished ${ago(finishedMs, now)}`, label: activity.label, ...times };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function creditsOf(status: ActivityStatus): StatusCredits | null {
|
|
133
|
+
const credits = status.credits;
|
|
134
|
+
if (credits === undefined || !plain(credits.usd, 16) || typeof credits.low !== "boolean") return null;
|
|
135
|
+
const checkedAt = iso(credits.checkedAt);
|
|
136
|
+
return checkedAt === undefined ? null : { usd: credits.usd, low: credits.low, checkedAt };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function statusData(input: StatusInput): StatusData {
|
|
140
|
+
const now = input.now.getTime();
|
|
141
|
+
const activity = activityOf(input.status, now);
|
|
142
|
+
const credits = creditsOf(input.status);
|
|
143
|
+
return {
|
|
144
|
+
product: "slopcamera",
|
|
145
|
+
version: input.version,
|
|
146
|
+
attention: activity.state === "failed" || credits?.low === true || input.legacyLogin.state === "found",
|
|
147
|
+
activity,
|
|
148
|
+
credits,
|
|
149
|
+
outputs: input.outputs,
|
|
150
|
+
legacyLoginItem: input.legacyLogin,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function statusNext(data: StatusData): NextStep[] {
|
|
155
|
+
const next: NextStep[] = [];
|
|
156
|
+
if (data.activity.state === "failed" || data.activity.state === "stale") {
|
|
157
|
+
next.push({ command: "slopcamera runs list --json", why: "See which run stopped and whether it can resume.", audience: "agent" });
|
|
158
|
+
}
|
|
159
|
+
if (data.credits?.low === true) {
|
|
160
|
+
next.push({ command: "slopcamera credits topup", why: "Credits are low; adding more opens a checkout for a person.", audience: "human" });
|
|
161
|
+
}
|
|
162
|
+
if (data.legacyLoginItem.state === "found") {
|
|
163
|
+
next.push({ command: "slopcamera legacy retire --json", why: "The retired menu bar still opens at login; this moves its login item aside.", audience: "agent" });
|
|
164
|
+
}
|
|
165
|
+
return next;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function statusEnvelope(input: StatusInput): Envelope<StatusData> {
|
|
169
|
+
const data = statusData(input);
|
|
170
|
+
const next = statusNext(data);
|
|
171
|
+
return okEnvelope(STATUS_SCHEMA, data, next.length === 0 ? undefined : next, input.now);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function size(bytes: number): string {
|
|
175
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
176
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
177
|
+
let value = bytes / 1024;
|
|
178
|
+
let unit = 0;
|
|
179
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
180
|
+
value /= 1024;
|
|
181
|
+
unit += 1;
|
|
182
|
+
}
|
|
183
|
+
return `${value < 10 ? Math.round(value * 10) / 10 : Math.round(value)} ${units[unit]}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function day(modifiedAt: string, now: number): string {
|
|
187
|
+
const then = Date.parse(modifiedAt);
|
|
188
|
+
const days = Math.floor((now - then) / 86_400_000);
|
|
189
|
+
if (days <= 0) return "today";
|
|
190
|
+
if (days === 1) return "yesterday";
|
|
191
|
+
return `${days} days ago`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function mark(state: ActivityState): string {
|
|
195
|
+
switch (state) {
|
|
196
|
+
case "idle": return "○";
|
|
197
|
+
case "running": return "↻";
|
|
198
|
+
case "done": return "✓";
|
|
199
|
+
default: return "⚠";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function loginLine(report: LegacyLoginReport): string {
|
|
204
|
+
switch (report.state) {
|
|
205
|
+
case "found": return "⚠ The old menu bar still opens at login · run slopcamera legacy retire";
|
|
206
|
+
case "not-ours": return "– A menu bar login item changed outside Slopcamera is left alone";
|
|
207
|
+
default: return "✓ Nothing starts at login";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The one Status view, rendered by both `tui` and `tui --snapshot`. */
|
|
212
|
+
export function statusViews(now: () => Date): View<StatusData>[] {
|
|
213
|
+
return [{
|
|
214
|
+
id: "status",
|
|
215
|
+
title: "Slopcamera",
|
|
216
|
+
render(data, width) {
|
|
217
|
+
const nowMs = now().getTime();
|
|
218
|
+
const lines = [`${mark(data.activity.state)} ${clean(data.activity.headline)} · ${clean(data.activity.detail)}`];
|
|
219
|
+
if (data.credits !== null) {
|
|
220
|
+
const checked = ago(Date.parse(data.credits.checkedAt), nowMs);
|
|
221
|
+
lines.push(data.credits.low
|
|
222
|
+
? `⚠ Credits are low · ${dollars(data.credits.usd)} left · checked ${checked}`
|
|
223
|
+
: `✓ ${dollars(data.credits.usd)} in credits · Checked ${checked}`);
|
|
224
|
+
}
|
|
225
|
+
const outputs = data.outputs.latest.length === 0
|
|
226
|
+
? ["No outputs yet · Agents save finished files here"]
|
|
227
|
+
: table(["Output", "Size", "Saved", "Type"], data.outputs.latest.map(item => [
|
|
228
|
+
clean(item.name), size(item.bytes), day(item.modifiedAt, nowMs), item.kind,
|
|
229
|
+
]), width - 2);
|
|
230
|
+
const more = data.outputs.total > data.outputs.latest.length
|
|
231
|
+
? [`${data.outputs.total} outputs · slopcamera outputs list --json`]
|
|
232
|
+
: ["slopcamera outputs open"];
|
|
233
|
+
return [
|
|
234
|
+
...box("Now", lines, width),
|
|
235
|
+
...box("Outputs", [...outputs, ...more], width),
|
|
236
|
+
...box("Login", [loginLine(data.legacyLoginItem)], width),
|
|
237
|
+
];
|
|
238
|
+
},
|
|
239
|
+
}];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function kindOf(name: string): string {
|
|
243
|
+
const extension = extname(name).slice(1);
|
|
244
|
+
return extension === "" || extension.length > 8 ? "File" : extension.toUpperCase();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The newest regular files at the top of the outputs folder. Hidden files,
|
|
249
|
+
* links and folders are skipped; a missing folder is an empty list.
|
|
250
|
+
*/
|
|
251
|
+
export function listOutputs(root: string, limit: number): StatusOutputs {
|
|
252
|
+
let names: string[];
|
|
253
|
+
try {
|
|
254
|
+
names = readdirSync(root);
|
|
255
|
+
} catch {
|
|
256
|
+
return { root, total: 0, latest: [] };
|
|
257
|
+
}
|
|
258
|
+
const files: StatusOutput[] = [];
|
|
259
|
+
for (const name of names) {
|
|
260
|
+
if (name.startsWith(".")) continue;
|
|
261
|
+
try {
|
|
262
|
+
const info = lstatSync(join(root, name));
|
|
263
|
+
if (!info.isFile()) continue;
|
|
264
|
+
files.push({ name, kind: kindOf(name), bytes: info.size, modifiedAt: new Date(info.mtimeMs).toISOString() });
|
|
265
|
+
} catch {
|
|
266
|
+
// Removed while listing.
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
files.sort((left, right) => right.modifiedAt.localeCompare(left.modifiedAt) || left.name.localeCompare(right.name));
|
|
270
|
+
return { root, total: files.length, latest: files.slice(0, limit) };
|
|
271
|
+
}
|
package/apps/desktop/cli/help.ts
CHANGED
|
@@ -39,6 +39,11 @@ Commands:
|
|
|
39
39
|
Buy and inspect hosted generation credits
|
|
40
40
|
media audio|color Apply local non-destructive audio and video effects
|
|
41
41
|
outputs Print the agent outputs directory
|
|
42
|
+
outputs list|open|reveal List the newest outputs or open one on this Mac
|
|
43
|
+
status What Slopcamera is doing, credits and newest outputs
|
|
44
|
+
tui [--snapshot] The status screen; --snapshot prints it once
|
|
45
|
+
commands [--json] List every command with what it may change
|
|
46
|
+
legacy retire Stop the old menu bar opening at login
|
|
42
47
|
recordings list List recording bundles
|
|
43
48
|
projects list|create List projects or create one from a recording
|
|
44
49
|
project inspect|add|edit|render
|
|
@@ -65,28 +70,6 @@ ${slopcameraSupportHelpLine()}`;
|
|
|
65
70
|
|
|
66
71
|
const HELP: Readonly<Record<string, string>> = {
|
|
67
72
|
advanced: `${slopcameraSupportAdvancedHelp()}`,
|
|
68
|
-
menubar: `Usage:
|
|
69
|
-
slopcamera menubar [--foreground|--background] [--json]
|
|
70
|
-
slopcamera menubar install|uninstall|status|start [--json]
|
|
71
|
-
|
|
72
|
-
Shows what Slopcamera is doing in the menu bar: a render in progress, how the
|
|
73
|
-
last job ended and your last known credits balance, plus your newest outputs.
|
|
74
|
-
It only reads that status and the outputs folder (slopcamera outputs).
|
|
75
|
-
|
|
76
|
-
menubar Open it now; it closes when this command ends
|
|
77
|
-
menubar start Open it now and return (same as --background)
|
|
78
|
-
menubar install Open it every time you log in
|
|
79
|
-
menubar uninstall Stop opening it at login and remove the installed copy
|
|
80
|
-
menubar status Check whether it opens at login and is running
|
|
81
|
-
|
|
82
|
-
If it's already in your menu bar, starting it again says so and succeeds.
|
|
83
|
-
macOS shows a notice that slopcamera-menubar can open at login. Turn it off any
|
|
84
|
-
time in System Settings › General › Login Items & Extensions.
|
|
85
|
-
|
|
86
|
-
The menu bar isn't in released packages yet, so it isn't listed in
|
|
87
|
-
slopcamera help. In a Slopcamera checkout, build it with
|
|
88
|
-
cargo build --release --manifest-path desktop/Cargo.toml and slopcamera finds
|
|
89
|
-
it; elsewhere, set SLOPCAMERA_MENUBAR to the built file.`,
|
|
90
73
|
capabilities: `Usage: slopcamera capabilities [--json]
|
|
91
74
|
|
|
92
75
|
Print the exact statically assembled capability manifest for this CLI build. Each module owns named operation, workflow, MCP-tool, command, and runtime-profile identities. The manifest includes trust classes, resource/effect policies, runtime requirements, qualification status, and a canonical SHA-256. It reads no workspace source, loads no plugin, probes no runtime, and grants no authority. Use doctor separately to inspect this machine's currently available executables.`,
|
|
@@ -729,7 +712,7 @@ export function commandHelp(topic: readonly string[]): string {
|
|
|
729
712
|
|
|
730
713
|
export function completions(words: readonly string[]): readonly string[] {
|
|
731
714
|
const topLevel = [
|
|
732
|
-
"capabilities", "operations", "diagram", "direct", "studio", "image", "html", "style", "workflows", "code", "runs", "doctor", "ai", "credits", "media", "
|
|
715
|
+
"capabilities", "operations", "diagram", "direct", "studio", "image", "html", "style", "workflows", "code", "runs", "doctor", "ai", "credits", "media", "support", "outputs", "status", "tui", "commands", "legacy", "recordings", "projects", "project", "scene", "inspect", "events", "edit", "analyze", "align", "faces", "fillers", "render", "assets",
|
|
733
716
|
];
|
|
734
717
|
if (words.length <= 1) return topLevel;
|
|
735
718
|
const command = words[0];
|
|
@@ -755,7 +738,8 @@ export function completions(words: readonly string[]): readonly string[] {
|
|
|
755
738
|
if (words[1] === "direction") return ["check", "plan", "gallery"];
|
|
756
739
|
if (words[1] === "effects") return ["check", "plan", "bake"];
|
|
757
740
|
if (words[1] === "behavior") return ["check", "bake", "gallery", "audit"];
|
|
758
|
-
|
|
741
|
+
if (words[1] === "design") return ["catalog", "init", "inspect", "set", "compile", "gallery"];
|
|
742
|
+
return ["init", "check", "inspect", "diff", "patch", "evaluate", "audit", "render-audit", "solve", "review", "camera-track", "generate", "plan", "render", "direction", "effects", "behavior", "design", "temporal-audit"];
|
|
759
743
|
}
|
|
760
744
|
if (command === "edit") return ["init", "show", "trim", "cut", "speed", "zoom", "overlay", "cursor", "clicks", "keystrokes", "typed-text"];
|
|
761
745
|
if (command === "analyze") return ["faces", "inactivity", "zooms", "music", "scenes", "speech"];
|
|
@@ -767,5 +751,7 @@ export function completions(words: readonly string[]): readonly string[] {
|
|
|
767
751
|
if (command === "media") return ["audio", "color"];
|
|
768
752
|
if (command === "render") return ["plan", "run"];
|
|
769
753
|
if (command === "assets") return ["emoji"];
|
|
754
|
+
if (command === "outputs") return ["list", "open", "reveal"];
|
|
755
|
+
if (command === "legacy") return ["retire"];
|
|
770
756
|
return [];
|
|
771
757
|
}
|