@agent-native/core 0.114.6 → 0.114.7
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
- package/corpus/core/src/cli/mcp.ts +124 -0
- package/corpus/core/src/cli/skills-content/help.ts +4 -3
- package/corpus/core/src/cli/skills-content/index.ts +1 -0
- package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
- package/corpus/core/src/cli/skills.ts +115 -13
- package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
- package/corpus/core/src/mcp-client/index.ts +9 -0
- package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
- package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
- package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
- package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
- package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
- package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
- package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
- package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
- package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
- package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
- package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
- package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
- package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
- package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
- package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
- package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
- package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
- package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
- package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
- package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
- package/corpus/templates/clips/desktop/src/app.tsx +2498 -239
- package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
- package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
- package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
- package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
- package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
- package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
- package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
- package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
- package/corpus/templates/clips/desktop/src/styles.css +956 -134
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
- package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
- package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
- package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
- package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
- package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
- package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
- package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
- package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
- package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
- package/dist/cli/mcp-config-writers.d.ts +2 -2
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +6 -6
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/cli/mcp.d.ts +14 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +79 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/skills-content/help.d.ts +1 -1
- package/dist/cli/skills-content/help.d.ts.map +1 -1
- package/dist/cli/skills-content/help.js +4 -3
- package/dist/cli/skills-content/help.js.map +1 -1
- package/dist/cli/skills-content/index.d.ts +1 -0
- package/dist/cli/skills-content/index.d.ts.map +1 -1
- package/dist/cli/skills-content/index.js +1 -0
- package/dist/cli/skills-content/index.js.map +1 -1
- package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
- package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
- package/dist/cli/skills-content/rewind-skill.js +53 -0
- package/dist/cli/skills-content/rewind-skill.js.map +1 -0
- package/dist/cli/skills.d.ts +6 -0
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +90 -14
- package/dist/cli/skills.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +365 -0
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp/screen-memory-stdio.js +1014 -91
- package/dist/mcp/screen-memory-stdio.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/screen-memory-local.d.ts +90 -0
- package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
- package/dist/mcp-client/screen-memory-local.js +590 -12
- package/dist/mcp-client/screen-memory-local.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/mcp-config-writers.ts +6 -4
- package/src/cli/mcp.ts +124 -0
- package/src/cli/skills-content/help.ts +4 -3
- package/src/cli/skills-content/index.ts +1 -0
- package/src/cli/skills-content/rewind-skill.ts +52 -0
- package/src/cli/skills.ts +115 -13
- package/src/mcp/screen-memory-stdio.ts +1360 -104
- package/src/mcp-client/index.ts +9 -0
- package/src/mcp-client/screen-memory-local.ts +847 -13
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
readAppState,
|
|
4
|
+
writeAppState,
|
|
5
|
+
} from "@agent-native/core/application-state";
|
|
6
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
import { isPrivateClip } from "../app/lib/rewind-visibility.js";
|
|
10
|
+
import { parseEdits, serializeEdits } from "../app/lib/timestamp-mapping.js";
|
|
11
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
12
|
+
import {
|
|
13
|
+
getCurrentOwnerEmail,
|
|
14
|
+
ownerEmailMatches,
|
|
15
|
+
} from "../server/lib/recordings.js";
|
|
16
|
+
import { parseTranscriptSegments } from "../shared/transcript-segments.js";
|
|
17
|
+
import { assertNoDirectRecordingShares } from "./make-recording-private-for-rewind.js";
|
|
18
|
+
import {
|
|
19
|
+
rewindExtensionKey,
|
|
20
|
+
type RewindExtensionRequest,
|
|
21
|
+
} from "./request-rewind-extension.js";
|
|
22
|
+
|
|
23
|
+
function shiftedJsonArray(
|
|
24
|
+
raw: string | null | undefined,
|
|
25
|
+
addedMs: number,
|
|
26
|
+
fields: string[],
|
|
27
|
+
): string {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw || "[]");
|
|
30
|
+
if (!Array.isArray(parsed)) return raw || "[]";
|
|
31
|
+
return JSON.stringify(
|
|
32
|
+
parsed.map((item) => {
|
|
33
|
+
if (!item || typeof item !== "object") return item;
|
|
34
|
+
const next = { ...item } as Record<string, unknown>;
|
|
35
|
+
for (const field of fields) {
|
|
36
|
+
if (typeof next[field] === "number") {
|
|
37
|
+
next[field] = Math.max(0, Math.round(next[field] + addedMs));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return next;
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
} catch {
|
|
44
|
+
return raw || "[]";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default defineAction({
|
|
49
|
+
description:
|
|
50
|
+
"Replace an owned Clip with an explicitly prepended local Rewind range while shifting its timestamped editor data and preserving the original-start marker.",
|
|
51
|
+
schema: z.object({
|
|
52
|
+
recordingId: z.string(),
|
|
53
|
+
requestId: z.string(),
|
|
54
|
+
preRollRecordingId: z.string(),
|
|
55
|
+
videoUrl: z.string().min(1),
|
|
56
|
+
durationMs: z.number().int().positive(),
|
|
57
|
+
addedMs: z
|
|
58
|
+
.number()
|
|
59
|
+
.int()
|
|
60
|
+
.positive()
|
|
61
|
+
.max(5 * 60_000),
|
|
62
|
+
}),
|
|
63
|
+
run: async (args) => {
|
|
64
|
+
if (args.videoUrl.startsWith("data:")) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"The combined Clip must be uploaded before it is applied.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const db = getDb();
|
|
70
|
+
const ownerEmail = getCurrentOwnerEmail();
|
|
71
|
+
const [recording] = await db
|
|
72
|
+
.select()
|
|
73
|
+
.from(schema.recordings)
|
|
74
|
+
.where(
|
|
75
|
+
and(
|
|
76
|
+
eq(schema.recordings.id, args.recordingId),
|
|
77
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
const [preRoll] = await db
|
|
81
|
+
.select()
|
|
82
|
+
.from(schema.recordings)
|
|
83
|
+
.where(
|
|
84
|
+
and(
|
|
85
|
+
eq(schema.recordings.id, args.preRollRecordingId),
|
|
86
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
if (!recording || !preRoll) {
|
|
90
|
+
throw new Error("The Clip or its local Rewind pre-roll is unavailable.");
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
!isPrivateClip(recording.visibility) ||
|
|
94
|
+
!isPrivateClip(preRoll.visibility)
|
|
95
|
+
) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"Rewind history can only be added to a private Clip from a private pre-roll.",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
await assertNoDirectRecordingShares(args.recordingId);
|
|
101
|
+
const key = rewindExtensionKey(args.recordingId);
|
|
102
|
+
const request = (await readAppState(key)) as RewindExtensionRequest | null;
|
|
103
|
+
if (
|
|
104
|
+
!request ||
|
|
105
|
+
request.requestId !== args.requestId ||
|
|
106
|
+
request.status !== "ready" ||
|
|
107
|
+
request.preRollRecordingId !== args.preRollRecordingId
|
|
108
|
+
) {
|
|
109
|
+
throw new Error("The Rewind pre-roll request is not ready to apply.");
|
|
110
|
+
}
|
|
111
|
+
const expectedDuration = recording.durationMs + args.addedMs;
|
|
112
|
+
if (Math.abs(args.durationMs - expectedDuration) > 2_000) {
|
|
113
|
+
throw new Error("The combined Clip duration does not match its sources.");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const edits = parseEdits(recording.editsJson);
|
|
117
|
+
edits.trims = edits.trims.map((trim) => ({
|
|
118
|
+
...trim,
|
|
119
|
+
startMs: trim.startMs + args.addedMs,
|
|
120
|
+
endMs: trim.endMs + args.addedMs,
|
|
121
|
+
}));
|
|
122
|
+
edits.blurs = edits.blurs.map((blur) => ({
|
|
123
|
+
...blur,
|
|
124
|
+
startMs: blur.startMs + args.addedMs,
|
|
125
|
+
endMs: blur.endMs + args.addedMs,
|
|
126
|
+
}));
|
|
127
|
+
edits.rewindOriginalStartMs = args.addedMs;
|
|
128
|
+
|
|
129
|
+
const [transcript] = await db
|
|
130
|
+
.select()
|
|
131
|
+
.from(schema.recordingTranscripts)
|
|
132
|
+
.where(eq(schema.recordingTranscripts.recordingId, args.recordingId));
|
|
133
|
+
const shiftedTranscript = transcript
|
|
134
|
+
? JSON.stringify(
|
|
135
|
+
parseTranscriptSegments(transcript.segmentsJson).map((segment) => ({
|
|
136
|
+
...segment,
|
|
137
|
+
startMs: segment.startMs + args.addedMs,
|
|
138
|
+
endMs: segment.endMs + args.addedMs,
|
|
139
|
+
})),
|
|
140
|
+
)
|
|
141
|
+
: null;
|
|
142
|
+
const now = new Date().toISOString();
|
|
143
|
+
|
|
144
|
+
await db.transaction(async (tx) => {
|
|
145
|
+
await tx
|
|
146
|
+
.update(schema.recordings)
|
|
147
|
+
.set({
|
|
148
|
+
videoUrl: args.videoUrl,
|
|
149
|
+
videoFormat: "mp4",
|
|
150
|
+
durationMs: args.durationMs,
|
|
151
|
+
editsJson: serializeEdits(edits),
|
|
152
|
+
chaptersJson: shiftedJsonArray(recording.chaptersJson, args.addedMs, [
|
|
153
|
+
"startMs",
|
|
154
|
+
]),
|
|
155
|
+
thumbnailUrl: null,
|
|
156
|
+
animatedThumbnailUrl: null,
|
|
157
|
+
updatedAt: now,
|
|
158
|
+
})
|
|
159
|
+
.where(eq(schema.recordings.id, args.recordingId));
|
|
160
|
+
if (transcript && shiftedTranscript) {
|
|
161
|
+
await tx
|
|
162
|
+
.update(schema.recordingTranscripts)
|
|
163
|
+
.set({ segmentsJson: shiftedTranscript, updatedAt: now })
|
|
164
|
+
.where(eq(schema.recordingTranscripts.recordingId, args.recordingId));
|
|
165
|
+
}
|
|
166
|
+
await tx
|
|
167
|
+
.update(schema.recordingComments)
|
|
168
|
+
.set({
|
|
169
|
+
videoTimestampMs: sql`${schema.recordingComments.videoTimestampMs} + ${args.addedMs}`,
|
|
170
|
+
updatedAt: now,
|
|
171
|
+
})
|
|
172
|
+
.where(eq(schema.recordingComments.recordingId, args.recordingId));
|
|
173
|
+
await tx
|
|
174
|
+
.update(schema.recordingReactions)
|
|
175
|
+
.set({
|
|
176
|
+
videoTimestampMs: sql`${schema.recordingReactions.videoTimestampMs} + ${args.addedMs}`,
|
|
177
|
+
})
|
|
178
|
+
.where(eq(schema.recordingReactions.recordingId, args.recordingId));
|
|
179
|
+
await tx
|
|
180
|
+
.update(schema.recordings)
|
|
181
|
+
.set({ trashedAt: now, expiresAt: now, updatedAt: now })
|
|
182
|
+
.where(eq(schema.recordings.id, args.preRollRecordingId));
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const applied: RewindExtensionRequest = {
|
|
186
|
+
...request,
|
|
187
|
+
status: "applied",
|
|
188
|
+
actualDurationMs: args.addedMs,
|
|
189
|
+
updatedAt: now,
|
|
190
|
+
};
|
|
191
|
+
await writeAppState(key, applied);
|
|
192
|
+
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
193
|
+
return { recordingId: args.recordingId, request: applied };
|
|
194
|
+
},
|
|
195
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import deleteRecordingPermanent from "./delete-recording-permanent.js";
|
|
8
|
+
|
|
9
|
+
export default defineAction({
|
|
10
|
+
description:
|
|
11
|
+
"Delete an expired agent-created Clip only if it is still private and has not been renamed, shared, commented on, reacted to, tagged, archived, or trashed.",
|
|
12
|
+
schema: z.object({ id: z.string() }),
|
|
13
|
+
run: async ({ id }) => {
|
|
14
|
+
await assertAccess("recording", id, "editor");
|
|
15
|
+
const db = getDb();
|
|
16
|
+
const [recording] = await db
|
|
17
|
+
.select()
|
|
18
|
+
.from(schema.recordings)
|
|
19
|
+
.where(eq(schema.recordings.id, id));
|
|
20
|
+
if (!recording) return { id, deleted: true, reason: "already-missing" };
|
|
21
|
+
|
|
22
|
+
const [share, comment, reaction, tag] = await Promise.all([
|
|
23
|
+
db
|
|
24
|
+
.select({ id: schema.recordingShares.id })
|
|
25
|
+
.from(schema.recordingShares)
|
|
26
|
+
.where(eq(schema.recordingShares.resourceId, id))
|
|
27
|
+
.limit(1),
|
|
28
|
+
db
|
|
29
|
+
.select({ id: schema.recordingComments.id })
|
|
30
|
+
.from(schema.recordingComments)
|
|
31
|
+
.where(eq(schema.recordingComments.recordingId, id))
|
|
32
|
+
.limit(1),
|
|
33
|
+
db
|
|
34
|
+
.select({ id: schema.recordingReactions.id })
|
|
35
|
+
.from(schema.recordingReactions)
|
|
36
|
+
.where(eq(schema.recordingReactions.recordingId, id))
|
|
37
|
+
.limit(1),
|
|
38
|
+
db
|
|
39
|
+
.select({ id: schema.recordingTags.id })
|
|
40
|
+
.from(schema.recordingTags)
|
|
41
|
+
.where(eq(schema.recordingTags.recordingId, id))
|
|
42
|
+
.limit(1),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const promoted =
|
|
46
|
+
recording.visibility !== "private" ||
|
|
47
|
+
recording.titleSource === "manual" ||
|
|
48
|
+
Boolean(recording.archivedAt || recording.trashedAt) ||
|
|
49
|
+
share.length > 0 ||
|
|
50
|
+
comment.length > 0 ||
|
|
51
|
+
reaction.length > 0 ||
|
|
52
|
+
tag.length > 0;
|
|
53
|
+
if (promoted) {
|
|
54
|
+
await db
|
|
55
|
+
.update(schema.recordings)
|
|
56
|
+
.set({ expiresAt: null, updatedAt: new Date().toISOString() })
|
|
57
|
+
.where(eq(schema.recordings.id, id));
|
|
58
|
+
return { id, deleted: false, reason: "promoted" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await deleteRecordingPermanent.run({ id });
|
|
62
|
+
return { id, deleted: true, reason: "retention-expired" };
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { readAppState } from "@agent-native/core/application-state";
|
|
3
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
rewindExtensionKey,
|
|
8
|
+
type RewindExtensionRequest,
|
|
9
|
+
} from "./request-rewind-extension.js";
|
|
10
|
+
|
|
11
|
+
export default defineAction({
|
|
12
|
+
description: "Read the current explicit Rewind pre-roll request for a Clip.",
|
|
13
|
+
schema: z.object({ recordingId: z.string() }),
|
|
14
|
+
http: { method: "GET" },
|
|
15
|
+
run: async ({ recordingId }) => {
|
|
16
|
+
await assertAccess("recording", recordingId, "owner");
|
|
17
|
+
const request = (await readAppState(
|
|
18
|
+
rewindExtensionKey(recordingId),
|
|
19
|
+
)) as RewindExtensionRequest | null;
|
|
20
|
+
return { request };
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -4,9 +4,25 @@ import { z } from "zod";
|
|
|
4
4
|
|
|
5
5
|
export default defineAction({
|
|
6
6
|
description:
|
|
7
|
-
"Check local Clips Screen Memory status for this machine: enabled/paused state, local
|
|
7
|
+
"Check local Clips Screen Memory status for this machine: enabled/paused state, local metadata files, and capture recency. Screen Memory is disabled by default, local-only, and does not expose media bytes or images.",
|
|
8
8
|
schema: z.object({}),
|
|
9
9
|
http: { method: "GET" },
|
|
10
10
|
readOnly: true,
|
|
11
|
-
run: async () =>
|
|
11
|
+
run: async () => {
|
|
12
|
+
const status = await readScreenMemoryStatus();
|
|
13
|
+
return {
|
|
14
|
+
feature: status.feature,
|
|
15
|
+
localOnly: status.localOnly,
|
|
16
|
+
enabled: status.enabled,
|
|
17
|
+
paused: status.paused,
|
|
18
|
+
state: status.state,
|
|
19
|
+
captureMode: status.config.captureMode,
|
|
20
|
+
retentionHours: status.config.retentionHours,
|
|
21
|
+
captureCount: status.captureCount,
|
|
22
|
+
storageBytes: status.storageBytes,
|
|
23
|
+
oldestCaptureAt: status.oldestCaptureAt,
|
|
24
|
+
newestCaptureAt: status.newestCaptureAt,
|
|
25
|
+
note: status.note,
|
|
26
|
+
};
|
|
27
|
+
},
|
|
12
28
|
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { listAppState } from "@agent-native/core/application-state";
|
|
3
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import {
|
|
8
|
+
getCurrentOwnerEmail,
|
|
9
|
+
ownerEmailMatches,
|
|
10
|
+
} from "../server/lib/recordings.js";
|
|
11
|
+
import {
|
|
12
|
+
REWIND_EXTENSION_PREFIX,
|
|
13
|
+
type RewindExtensionRequest,
|
|
14
|
+
} from "./request-rewind-extension.js";
|
|
15
|
+
|
|
16
|
+
export default defineAction({
|
|
17
|
+
description:
|
|
18
|
+
"List pending local Rewind pre-roll requests for the signed-in Clips Alpha app.",
|
|
19
|
+
schema: z.object({}),
|
|
20
|
+
http: { method: "GET" },
|
|
21
|
+
run: async () => {
|
|
22
|
+
const entries = await listAppState(REWIND_EXTENSION_PREFIX);
|
|
23
|
+
const staleProcessingCutoff = Date.now() - 2 * 60_000;
|
|
24
|
+
const requests = entries
|
|
25
|
+
.map((entry) => entry.value as unknown as RewindExtensionRequest)
|
|
26
|
+
.filter(
|
|
27
|
+
(request) =>
|
|
28
|
+
request &&
|
|
29
|
+
typeof request.recordingId === "string" &&
|
|
30
|
+
(request.status === "pending" ||
|
|
31
|
+
(request.status === "processing" &&
|
|
32
|
+
Date.parse(request.updatedAt) < staleProcessingCutoff)),
|
|
33
|
+
);
|
|
34
|
+
if (!requests.length) return { requests: [] };
|
|
35
|
+
const ids = [...new Set(requests.map((request) => request.recordingId))];
|
|
36
|
+
const ownerEmail = getCurrentOwnerEmail();
|
|
37
|
+
const accessible = await getDb()
|
|
38
|
+
.select({ id: schema.recordings.id })
|
|
39
|
+
.from(schema.recordings)
|
|
40
|
+
.where(
|
|
41
|
+
and(
|
|
42
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
43
|
+
inArray(schema.recordings.id, ids),
|
|
44
|
+
eq(schema.recordings.status, "ready"),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
const allowed = new Set(accessible.map((row) => row.id));
|
|
48
|
+
return {
|
|
49
|
+
requests: requests.filter((request) => allowed.has(request.recordingId)),
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, eq, notExists } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
|
|
8
|
+
export const DIRECT_SHARE_REWIND_ERROR =
|
|
9
|
+
"This Clip is still shared directly with other people. Remove their access in Share before adding local Rewind history.";
|
|
10
|
+
|
|
11
|
+
export async function assertNoDirectRecordingShares(
|
|
12
|
+
recordingId: string,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const [share] = await getDb()
|
|
15
|
+
.select({ id: schema.recordingShares.id })
|
|
16
|
+
.from(schema.recordingShares)
|
|
17
|
+
.where(eq(schema.recordingShares.resourceId, recordingId))
|
|
18
|
+
.limit(1);
|
|
19
|
+
if (share) throw new Error(DIRECT_SHARE_REWIND_ERROR);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default defineAction({
|
|
23
|
+
description:
|
|
24
|
+
"Make an owned recording private for a Rewind extension, refusing to continue while anyone still has direct access.",
|
|
25
|
+
schema: z.object({ recordingId: z.string() }),
|
|
26
|
+
run: async ({ recordingId }) => {
|
|
27
|
+
await assertAccess("recording", recordingId, "owner");
|
|
28
|
+
|
|
29
|
+
const now = new Date().toISOString();
|
|
30
|
+
const db = getDb();
|
|
31
|
+
const [recording] = await db
|
|
32
|
+
.update(schema.recordings)
|
|
33
|
+
.set({ visibility: "private", updatedAt: now })
|
|
34
|
+
.where(
|
|
35
|
+
and(
|
|
36
|
+
eq(schema.recordings.id, recordingId),
|
|
37
|
+
notExists(
|
|
38
|
+
db
|
|
39
|
+
.select({ id: schema.recordingShares.id })
|
|
40
|
+
.from(schema.recordingShares)
|
|
41
|
+
.where(eq(schema.recordingShares.resourceId, recordingId)),
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
.returning({ id: schema.recordings.id });
|
|
46
|
+
if (!recording) {
|
|
47
|
+
// The conditional UPDATE is the invariant. This follow-up read only
|
|
48
|
+
// chooses the useful error message after the atomic mutation declined.
|
|
49
|
+
await assertNoDirectRecordingShares(recordingId);
|
|
50
|
+
throw new Error("This Clip is unavailable.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { recordingId, visibility: "private" as const };
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
-
import {
|
|
2
|
+
import { queryScreenMemoryForAgent } from "@agent-native/core/mcp-client";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
export default defineAction({
|
|
6
6
|
description:
|
|
7
|
-
"Search
|
|
7
|
+
"Search bounded local Clips Screen Memory evidence from this machine. Returns typed app-context, OCR, or transcript excerpts only when present in local files, plus coverage, gaps, segment references, jump targets, and truncation; never media bytes or images. Use get-screen-memory-status first if availability is unclear.",
|
|
8
8
|
schema: z.object({
|
|
9
9
|
query: z
|
|
10
10
|
.string()
|
|
@@ -21,9 +21,9 @@ export default defineAction({
|
|
|
21
21
|
.min(1)
|
|
22
22
|
.max(50)
|
|
23
23
|
.default(10)
|
|
24
|
-
.describe("Maximum number of
|
|
24
|
+
.describe("Maximum number of bounded local evidence items to return"),
|
|
25
25
|
}),
|
|
26
26
|
http: { method: "GET" },
|
|
27
27
|
readOnly: true,
|
|
28
|
-
run: async (args) =>
|
|
28
|
+
run: async (args) => queryScreenMemoryForAgent(args),
|
|
29
29
|
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
readAppState,
|
|
4
|
+
writeAppState,
|
|
5
|
+
} from "@agent-native/core/application-state";
|
|
6
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
7
|
+
import { eq } from "drizzle-orm";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import { isPrivateClip } from "../app/lib/rewind-visibility.js";
|
|
11
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
12
|
+
import { nanoid } from "../server/lib/recordings.js";
|
|
13
|
+
import { assertNoDirectRecordingShares } from "./make-recording-private-for-rewind.js";
|
|
14
|
+
|
|
15
|
+
export const REWIND_EXTENSION_PREFIX = "rewind-extension-request-";
|
|
16
|
+
|
|
17
|
+
export interface RewindExtensionRequest {
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
requestId: string;
|
|
20
|
+
recordingId: string;
|
|
21
|
+
seconds: 30 | 300;
|
|
22
|
+
status:
|
|
23
|
+
| "pending"
|
|
24
|
+
| "processing"
|
|
25
|
+
| "ready"
|
|
26
|
+
| "applying"
|
|
27
|
+
| "applied"
|
|
28
|
+
| "failed";
|
|
29
|
+
requestedAt: string;
|
|
30
|
+
updatedAt: string;
|
|
31
|
+
preRollRecordingId?: string;
|
|
32
|
+
actualDurationMs?: number;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function rewindExtensionKey(recordingId: string): string {
|
|
37
|
+
return `${REWIND_EXTENSION_PREFIX}${recordingId}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default defineAction({
|
|
41
|
+
description:
|
|
42
|
+
"Ask the signed-in Clips Alpha app to retrieve an explicit 30-second or five-minute pre-roll from local Rewind for one owned recording.",
|
|
43
|
+
schema: z.object({
|
|
44
|
+
recordingId: z.string(),
|
|
45
|
+
seconds: z.union([z.literal(30), z.literal(300)]),
|
|
46
|
+
}),
|
|
47
|
+
run: async ({ recordingId, seconds }) => {
|
|
48
|
+
await assertAccess("recording", recordingId, "owner");
|
|
49
|
+
const [recording] = await getDb()
|
|
50
|
+
.select({ visibility: schema.recordings.visibility })
|
|
51
|
+
.from(schema.recordings)
|
|
52
|
+
.where(eq(schema.recordings.id, recordingId));
|
|
53
|
+
if (!recording || !isPrivateClip(recording.visibility)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"Make this Clip private before adding local Rewind history.",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
await assertNoDirectRecordingShares(recordingId);
|
|
59
|
+
const key = rewindExtensionKey(recordingId);
|
|
60
|
+
const existing = (await readAppState(key)) as RewindExtensionRequest | null;
|
|
61
|
+
if (existing && !["failed", "applied"].includes(existing.status)) {
|
|
62
|
+
return existing;
|
|
63
|
+
}
|
|
64
|
+
if (existing?.status === "applied") {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"This Clip already includes an explicit Rewind pre-roll.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const now = new Date().toISOString();
|
|
70
|
+
const request: RewindExtensionRequest = {
|
|
71
|
+
requestId: `rewind-extend-${nanoid()}`,
|
|
72
|
+
recordingId,
|
|
73
|
+
seconds,
|
|
74
|
+
status: "pending",
|
|
75
|
+
requestedAt: now,
|
|
76
|
+
updatedAt: now,
|
|
77
|
+
};
|
|
78
|
+
await writeAppState(key, request);
|
|
79
|
+
return request;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
readAppState,
|
|
4
|
+
writeAppState,
|
|
5
|
+
} from "@agent-native/core/application-state";
|
|
6
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
rewindExtensionKey,
|
|
11
|
+
type RewindExtensionRequest,
|
|
12
|
+
} from "./request-rewind-extension.js";
|
|
13
|
+
|
|
14
|
+
export default defineAction({
|
|
15
|
+
description:
|
|
16
|
+
"Update the local Clips Alpha processing state for an explicit Rewind pre-roll request.",
|
|
17
|
+
schema: z.object({
|
|
18
|
+
recordingId: z.string(),
|
|
19
|
+
requestId: z.string(),
|
|
20
|
+
status: z.enum(["processing", "ready", "failed"]),
|
|
21
|
+
preRollRecordingId: z.string().optional(),
|
|
22
|
+
actualDurationMs: z.number().int().positive().optional(),
|
|
23
|
+
error: z.string().max(1200).optional(),
|
|
24
|
+
}),
|
|
25
|
+
run: async (args) => {
|
|
26
|
+
await assertAccess("recording", args.recordingId, "owner");
|
|
27
|
+
const key = rewindExtensionKey(args.recordingId);
|
|
28
|
+
const current = (await readAppState(key)) as RewindExtensionRequest | null;
|
|
29
|
+
if (!current || current.requestId !== args.requestId) {
|
|
30
|
+
throw new Error("Rewind extension request is no longer current.");
|
|
31
|
+
}
|
|
32
|
+
const next: RewindExtensionRequest = {
|
|
33
|
+
...current,
|
|
34
|
+
status: args.status,
|
|
35
|
+
updatedAt: new Date().toISOString(),
|
|
36
|
+
...(args.preRollRecordingId
|
|
37
|
+
? { preRollRecordingId: args.preRollRecordingId }
|
|
38
|
+
: {}),
|
|
39
|
+
...(args.actualDurationMs
|
|
40
|
+
? { actualDurationMs: args.actualDurationMs }
|
|
41
|
+
: {}),
|
|
42
|
+
...(args.error ? { error: args.error } : {}),
|
|
43
|
+
};
|
|
44
|
+
await writeAppState(key, next);
|
|
45
|
+
return next;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
readPlaybackSpeedPreference,
|
|
50
50
|
savePlaybackSpeedPreference,
|
|
51
51
|
} from "@/lib/playback-speed";
|
|
52
|
+
import { canOfferRewindHistory } from "@/lib/rewind-visibility";
|
|
52
53
|
import {
|
|
53
54
|
parseEdits,
|
|
54
55
|
getExcludedRanges,
|
|
@@ -62,6 +63,7 @@ import { computePeaks, type WaveformPeaks } from "@/lib/waveform-peaks";
|
|
|
62
63
|
import { ChaptersEditor } from "./chapters-editor";
|
|
63
64
|
import { defaultSelectionRange } from "./editor-selection";
|
|
64
65
|
import { EditorToolbar } from "./editor-toolbar";
|
|
66
|
+
import { RewindExtensionDialog } from "./rewind-extension-dialog";
|
|
65
67
|
import { StitchManager } from "./stitch-manager";
|
|
66
68
|
import { ThumbnailPicker } from "./thumbnail-picker";
|
|
67
69
|
import { Timeline } from "./timeline";
|
|
@@ -219,6 +221,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
|
|
|
219
221
|
|
|
220
222
|
const [thumbOpen, setThumbOpen] = useState(false);
|
|
221
223
|
const [stitchOpen, setStitchOpen] = useState(false);
|
|
224
|
+
const [rewindOpen, setRewindOpen] = useState(false);
|
|
222
225
|
const [chaptersOpen, setChaptersOpen] = useState(false);
|
|
223
226
|
|
|
224
227
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -636,6 +639,10 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
|
|
|
636
639
|
onOpenThumbnailPicker={() => setThumbOpen(true)}
|
|
637
640
|
onOpenChapters={() => setChaptersOpen((v) => !v)}
|
|
638
641
|
onOpenStitch={() => setStitchOpen(true)}
|
|
642
|
+
onOpenRewind={() => setRewindOpen(true)}
|
|
643
|
+
rewindAlreadyAdded={Boolean(edits.rewindOriginalStartMs)}
|
|
644
|
+
rewindAvailable={canOfferRewindHistory(playerData?.role)}
|
|
645
|
+
rewindRequiresPrivate={recording?.visibility !== "private"}
|
|
639
646
|
chaptersOpen={chaptersOpen}
|
|
640
647
|
/>
|
|
641
648
|
|
|
@@ -739,6 +746,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
|
|
|
739
746
|
playheadMs={playheadMs}
|
|
740
747
|
chapters={chapters}
|
|
741
748
|
splitPoints={splitPoints}
|
|
749
|
+
originalStartMs={edits.rewindOriginalStartMs}
|
|
742
750
|
onSeek={seek}
|
|
743
751
|
onClickChapter={(c) => seek(c.startMs)}
|
|
744
752
|
/>
|
|
@@ -787,6 +795,23 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
|
|
|
787
795
|
onOpenChange={setStitchOpen}
|
|
788
796
|
seedRecordingId={recordingId}
|
|
789
797
|
/>
|
|
798
|
+
{canOfferRewindHistory(playerData?.role) ? (
|
|
799
|
+
<RewindExtensionDialog
|
|
800
|
+
open={rewindOpen}
|
|
801
|
+
onOpenChange={setRewindOpen}
|
|
802
|
+
recordingId={recordingId}
|
|
803
|
+
durationMs={durationMs}
|
|
804
|
+
videoFormat={videoFormat}
|
|
805
|
+
hasAudio={Boolean(recording.hasAudio)}
|
|
806
|
+
visibility={recording.visibility}
|
|
807
|
+
onVisibilityChanged={async () => {
|
|
808
|
+
await playerDataQuery.refetch();
|
|
809
|
+
}}
|
|
810
|
+
onApplied={async () => {
|
|
811
|
+
await playerDataQuery.refetch();
|
|
812
|
+
}}
|
|
813
|
+
/>
|
|
814
|
+
) : null}
|
|
790
815
|
</div>
|
|
791
816
|
);
|
|
792
817
|
}
|