@anscribe/core 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 msmps
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @anscribe/core
2
+
3
+ > Internal capture model shared by `@anscribe/opentui`, `@anscribe/mcp`, and `@anscribe/react`. **Not part of the public API.**
4
+
5
+ You almost certainly want [`@anscribe/opentui`](https://www.npmjs.com/package/@anscribe/opentui).
6
+
7
+ This package exists so the OpenTUI host, the MCP server, and the React enricher can share Schema definitions (`Capture`, `CapturedTarget`, `TerminalCellBounds`, …) and the framework-agnostic Capture Mode state machine without circular dependencies. Its surface is not stabilized; minor releases may break consumers.
8
+
9
+ ## License
10
+
11
+ MIT © [msmps](https://github.com/msmps)
@@ -0,0 +1,405 @@
1
+ import { Context, Data, Effect, FileSystem, Layer, Path, Schema } from "effect";
2
+ import * as _$effect_Brand0 from "effect/Brand";
3
+ import * as _$effect_Cause0 from "effect/Cause";
4
+ import * as _$effect_Unify0 from "effect/Unify";
5
+
6
+ //#region src/schema.d.ts
7
+ declare const CaptureId: Schema.brand<Schema.String, "CaptureId">;
8
+ type CaptureId = typeof CaptureId.Type;
9
+ declare const CapturedTargetId: Schema.brand<Schema.String, "CapturedTargetId">;
10
+ type CapturedTargetId = typeof CapturedTargetId.Type;
11
+ declare const generateCaptureId: Effect.Effect<string & _$effect_Brand0.Brand<"CaptureId">, never, never>;
12
+ declare const generateCapturedTargetId: Effect.Effect<string & _$effect_Brand0.Brand<"CapturedTargetId">, never, never>;
13
+ declare function parseIsoTimestampMillis(value: string): number | undefined;
14
+ declare const IsoTimestamp: Schema.brand<Schema.String, "IsoTimestamp">;
15
+ type IsoTimestamp = typeof IsoTimestamp.Type;
16
+ declare const CaptureValidationError_base: Schema.Class<CaptureValidationError, Schema.TaggedStruct<"CaptureValidationError", {
17
+ readonly message: Schema.String;
18
+ readonly cause: Schema.optionalKey<Schema.Unknown>;
19
+ }>, _$effect_Cause0.YieldableError>;
20
+ declare class CaptureValidationError extends CaptureValidationError_base {}
21
+ declare const CapturePersistenceError_base: Schema.Class<CapturePersistenceError, Schema.TaggedStruct<"CapturePersistenceError", {
22
+ readonly message: Schema.String;
23
+ readonly cause: Schema.optionalKey<Schema.Unknown>;
24
+ }>, _$effect_Cause0.YieldableError>;
25
+ declare class CapturePersistenceError extends CapturePersistenceError_base {}
26
+ declare const TerminalCellBounds_base: Schema.Class<TerminalCellBounds, Schema.Struct<{
27
+ readonly x: Schema.Finite;
28
+ readonly y: Schema.Finite;
29
+ readonly width: Schema.Finite;
30
+ readonly height: Schema.Finite;
31
+ }>, {}>;
32
+ declare class TerminalCellBounds extends TerminalCellBounds_base {}
33
+ declare const SourceReference_base: Schema.Class<SourceReference, Schema.Struct<{
34
+ readonly file: Schema.optionalKey<Schema.String>;
35
+ readonly line: Schema.optionalKey<Schema.Finite>;
36
+ readonly column: Schema.optionalKey<Schema.Finite>;
37
+ readonly functionName: Schema.optionalKey<Schema.String>;
38
+ readonly componentName: Schema.optionalKey<Schema.String>;
39
+ readonly origin: Schema.optionalKey<Schema.String>;
40
+ }>, {}>;
41
+ declare class SourceReference extends SourceReference_base {}
42
+ declare const CaptureMetadata_base: Schema.Class<CaptureMetadata, Schema.Struct<{
43
+ readonly identifier: Schema.optionalKey<Schema.String>;
44
+ readonly componentName: Schema.optionalKey<Schema.String>;
45
+ readonly componentPath: Schema.optionalKey<Schema.String>;
46
+ }>, {}>;
47
+ declare class CaptureMetadata extends CaptureMetadata_base {}
48
+ type CaptureStatus = "pending" | "resolved";
49
+ declare const captureStatusSchema: Schema.Literals<readonly ["pending", "resolved"]>;
50
+ declare const CapturedTarget_base: Schema.Class<CapturedTarget, Schema.Struct<{
51
+ readonly id: Schema.brand<Schema.String, "CapturedTargetId">;
52
+ readonly type: Schema.String;
53
+ readonly bounds: typeof TerminalCellBounds;
54
+ readonly ancestry: Schema.$Array<Schema.String>;
55
+ readonly visibleContent: Schema.optionalKey<Schema.String>;
56
+ readonly metadata: Schema.optionalKey<typeof CaptureMetadata>;
57
+ readonly sourceReferences: Schema.optionalKey<Schema.$Array<typeof SourceReference>>;
58
+ }>, {}>;
59
+ declare class CapturedTarget extends CapturedTarget_base {}
60
+ declare const Capture_base: Schema.Class<Capture, Schema.Struct<{
61
+ readonly id: Schema.brand<Schema.String, "CaptureId">;
62
+ readonly status: Schema.Literals<readonly ["pending", "resolved"]>;
63
+ readonly createdAt: Schema.brand<Schema.String, "IsoTimestamp">;
64
+ readonly instruction: Schema.optionalKey<Schema.String>;
65
+ readonly targets: Schema.$Array<typeof CapturedTarget>;
66
+ }>, {}>;
67
+ declare class Capture extends Capture_base {}
68
+ type CaptureInput = ConstructorParameters<typeof Capture>[0];
69
+ declare const makeCapture: (input: CaptureInput) => Effect.Effect<Capture, CaptureValidationError, never>;
70
+ declare function decodeAnscribeData<T>(schema: Schema.Schema<T>, input: unknown): T;
71
+ declare const decodeAnscribeDataEffect: <S extends Schema.Top>(schema: S, input: unknown) => Effect.Effect<S["Type"], CaptureValidationError, S["DecodingServices"]>;
72
+ //#endregion
73
+ //#region src/project.d.ts
74
+ declare const CaptureProjectResolutionError_base: Schema.Class<CaptureProjectResolutionError, Schema.TaggedStruct<"CaptureProjectResolutionError", {
75
+ readonly message: Schema.String;
76
+ readonly cause: Schema.optionalKey<Schema.Unknown>;
77
+ }>, _$effect_Cause0.YieldableError>;
78
+ declare class CaptureProjectResolutionError extends CaptureProjectResolutionError_base {}
79
+ interface ResolvedCaptureProject {
80
+ readonly projectRoot: string;
81
+ }
82
+ declare const resolveCaptureProjectBoundary: (cwd: string) => Effect.Effect<ResolvedCaptureProject, CaptureProjectResolutionError, FileSystem.FileSystem | Path.Path>;
83
+ //#endregion
84
+ //#region src/enrichment.d.ts
85
+ interface CaptureEnrichmentOutput {
86
+ readonly metadata?: CaptureMetadata;
87
+ readonly sourceReferences?: readonly SourceReference[];
88
+ }
89
+ declare const noCaptureEnrichment: Effect.Effect<CaptureEnrichmentOutput | undefined>;
90
+ type CaptureMetadataEnricher = (input: {
91
+ renderable: unknown;
92
+ target: CapturedTarget;
93
+ }) => Effect.Effect<CaptureEnrichmentOutput | undefined>;
94
+ interface CaptureMetadataEnrichmentShape {
95
+ readonly enrich: CaptureMetadataEnricher;
96
+ }
97
+ declare const CaptureMetadataEnrichment_base: Context.ServiceClass<CaptureMetadataEnrichment, "anscribe/enrichment/CaptureMetadataEnrichment", CaptureMetadataEnrichmentShape>;
98
+ declare class CaptureMetadataEnrichment extends CaptureMetadataEnrichment_base {
99
+ static readonly live: Layer.Layer<CaptureMetadataEnrichment, never, never>;
100
+ static readonly layer: (enricher: CaptureMetadataEnricher) => Layer.Layer<CaptureMetadataEnrichment, never, never>;
101
+ }
102
+ //#endregion
103
+ //#region src/picker/state.d.ts
104
+ interface PickerState {
105
+ readonly active: boolean;
106
+ readonly targets: readonly CapturedTarget[];
107
+ readonly currentIndex: number;
108
+ readonly selectedTargetIds: readonly CapturedTargetId[];
109
+ }
110
+ declare function selectCurrentTarget(state: PickerState): CapturedTarget | undefined;
111
+ declare function resolveTargetAtCell(targets: readonly CapturedTarget[], x: number, y: number): CapturedTarget | undefined;
112
+ //#endregion
113
+ //#region src/capture-mode/state.d.ts
114
+ interface CaptureModeState extends PickerState {
115
+ readonly instructionDraft: boolean;
116
+ }
117
+ declare const initialState: CaptureModeState;
118
+ type CaptureModeIntent = Data.TaggedEnum<{
119
+ EnterMode: {
120
+ readonly targets: readonly CapturedTarget[];
121
+ };
122
+ ExitMode: {};
123
+ MoveSelection: {
124
+ readonly direction: "next" | "previous";
125
+ };
126
+ DeselectCurrent: {};
127
+ ToggleCurrent: {};
128
+ SelectAtCell: {
129
+ readonly x: number;
130
+ readonly y: number;
131
+ };
132
+ StartDraft: {};
133
+ CommitDraft: {
134
+ readonly body: string;
135
+ };
136
+ CancelDraft: {};
137
+ }>;
138
+ declare const CaptureModeIntent: {
139
+ readonly EnterMode: Data.TaggedEnum.ConstructorFrom<{
140
+ readonly _tag: "EnterMode";
141
+ readonly targets: readonly CapturedTarget[];
142
+ }, "_tag">;
143
+ readonly ExitMode: Data.TaggedEnum.ConstructorFrom<{
144
+ readonly _tag: "ExitMode";
145
+ }, "_tag">;
146
+ readonly MoveSelection: Data.TaggedEnum.ConstructorFrom<{
147
+ readonly _tag: "MoveSelection";
148
+ readonly direction: "next" | "previous";
149
+ }, "_tag">;
150
+ readonly DeselectCurrent: Data.TaggedEnum.ConstructorFrom<{
151
+ readonly _tag: "DeselectCurrent";
152
+ }, "_tag">;
153
+ readonly ToggleCurrent: Data.TaggedEnum.ConstructorFrom<{
154
+ readonly _tag: "ToggleCurrent";
155
+ }, "_tag">;
156
+ readonly SelectAtCell: Data.TaggedEnum.ConstructorFrom<{
157
+ readonly _tag: "SelectAtCell";
158
+ readonly x: number;
159
+ readonly y: number;
160
+ }, "_tag">;
161
+ readonly StartDraft: Data.TaggedEnum.ConstructorFrom<{
162
+ readonly _tag: "StartDraft";
163
+ }, "_tag">;
164
+ readonly CommitDraft: Data.TaggedEnum.ConstructorFrom<{
165
+ readonly _tag: "CommitDraft";
166
+ readonly body: string;
167
+ }, "_tag">;
168
+ readonly CancelDraft: Data.TaggedEnum.ConstructorFrom<{
169
+ readonly _tag: "CancelDraft";
170
+ }, "_tag">;
171
+ readonly $is: <Tag extends "EnterMode" | "ExitMode" | "MoveSelection" | "DeselectCurrent" | "ToggleCurrent" | "SelectAtCell" | "StartDraft" | "CommitDraft" | "CancelDraft">(tag: Tag) => (u: unknown) => u is Extract<{
172
+ readonly _tag: "EnterMode";
173
+ readonly targets: readonly CapturedTarget[];
174
+ }, {
175
+ readonly _tag: Tag;
176
+ }> | Extract<{
177
+ readonly _tag: "ExitMode";
178
+ }, {
179
+ readonly _tag: Tag;
180
+ }> | Extract<{
181
+ readonly _tag: "MoveSelection";
182
+ readonly direction: "next" | "previous";
183
+ }, {
184
+ readonly _tag: Tag;
185
+ }> | Extract<{
186
+ readonly _tag: "DeselectCurrent";
187
+ }, {
188
+ readonly _tag: Tag;
189
+ }> | Extract<{
190
+ readonly _tag: "ToggleCurrent";
191
+ }, {
192
+ readonly _tag: Tag;
193
+ }> | Extract<{
194
+ readonly _tag: "SelectAtCell";
195
+ readonly x: number;
196
+ readonly y: number;
197
+ }, {
198
+ readonly _tag: Tag;
199
+ }> | Extract<{
200
+ readonly _tag: "StartDraft";
201
+ }, {
202
+ readonly _tag: Tag;
203
+ }> | Extract<{
204
+ readonly _tag: "CommitDraft";
205
+ readonly body: string;
206
+ }, {
207
+ readonly _tag: Tag;
208
+ }> | Extract<{
209
+ readonly _tag: "CancelDraft";
210
+ }, {
211
+ readonly _tag: Tag;
212
+ }>;
213
+ readonly $match: {
214
+ <Cases extends {
215
+ readonly EnterMode: (args: {
216
+ readonly _tag: "EnterMode";
217
+ readonly targets: readonly CapturedTarget[];
218
+ }) => any;
219
+ readonly ExitMode: (args: {
220
+ readonly _tag: "ExitMode";
221
+ }) => any;
222
+ readonly MoveSelection: (args: {
223
+ readonly _tag: "MoveSelection";
224
+ readonly direction: "next" | "previous";
225
+ }) => any;
226
+ readonly DeselectCurrent: (args: {
227
+ readonly _tag: "DeselectCurrent";
228
+ }) => any;
229
+ readonly ToggleCurrent: (args: {
230
+ readonly _tag: "ToggleCurrent";
231
+ }) => any;
232
+ readonly SelectAtCell: (args: {
233
+ readonly _tag: "SelectAtCell";
234
+ readonly x: number;
235
+ readonly y: number;
236
+ }) => any;
237
+ readonly StartDraft: (args: {
238
+ readonly _tag: "StartDraft";
239
+ }) => any;
240
+ readonly CommitDraft: (args: {
241
+ readonly _tag: "CommitDraft";
242
+ readonly body: string;
243
+ }) => any;
244
+ readonly CancelDraft: (args: {
245
+ readonly _tag: "CancelDraft";
246
+ }) => any;
247
+ }>(cases: Cases): (value: {
248
+ readonly _tag: "EnterMode";
249
+ readonly targets: readonly CapturedTarget[];
250
+ } | {
251
+ readonly _tag: "ExitMode";
252
+ } | {
253
+ readonly _tag: "MoveSelection";
254
+ readonly direction: "next" | "previous";
255
+ } | {
256
+ readonly _tag: "DeselectCurrent";
257
+ } | {
258
+ readonly _tag: "ToggleCurrent";
259
+ } | {
260
+ readonly _tag: "SelectAtCell";
261
+ readonly x: number;
262
+ readonly y: number;
263
+ } | {
264
+ readonly _tag: "StartDraft";
265
+ } | {
266
+ readonly _tag: "CommitDraft";
267
+ readonly body: string;
268
+ } | {
269
+ readonly _tag: "CancelDraft";
270
+ }) => _$effect_Unify0.Unify<ReturnType<Cases["EnterMode" | "ExitMode" | "MoveSelection" | "DeselectCurrent" | "ToggleCurrent" | "SelectAtCell" | "StartDraft" | "CommitDraft" | "CancelDraft"]>>;
271
+ <Cases extends {
272
+ readonly EnterMode: (args: {
273
+ readonly _tag: "EnterMode";
274
+ readonly targets: readonly CapturedTarget[];
275
+ }) => any;
276
+ readonly ExitMode: (args: {
277
+ readonly _tag: "ExitMode";
278
+ }) => any;
279
+ readonly MoveSelection: (args: {
280
+ readonly _tag: "MoveSelection";
281
+ readonly direction: "next" | "previous";
282
+ }) => any;
283
+ readonly DeselectCurrent: (args: {
284
+ readonly _tag: "DeselectCurrent";
285
+ }) => any;
286
+ readonly ToggleCurrent: (args: {
287
+ readonly _tag: "ToggleCurrent";
288
+ }) => any;
289
+ readonly SelectAtCell: (args: {
290
+ readonly _tag: "SelectAtCell";
291
+ readonly x: number;
292
+ readonly y: number;
293
+ }) => any;
294
+ readonly StartDraft: (args: {
295
+ readonly _tag: "StartDraft";
296
+ }) => any;
297
+ readonly CommitDraft: (args: {
298
+ readonly _tag: "CommitDraft";
299
+ readonly body: string;
300
+ }) => any;
301
+ readonly CancelDraft: (args: {
302
+ readonly _tag: "CancelDraft";
303
+ }) => any;
304
+ }>(value: {
305
+ readonly _tag: "EnterMode";
306
+ readonly targets: readonly CapturedTarget[];
307
+ } | {
308
+ readonly _tag: "ExitMode";
309
+ } | {
310
+ readonly _tag: "MoveSelection";
311
+ readonly direction: "next" | "previous";
312
+ } | {
313
+ readonly _tag: "DeselectCurrent";
314
+ } | {
315
+ readonly _tag: "ToggleCurrent";
316
+ } | {
317
+ readonly _tag: "SelectAtCell";
318
+ readonly x: number;
319
+ readonly y: number;
320
+ } | {
321
+ readonly _tag: "StartDraft";
322
+ } | {
323
+ readonly _tag: "CommitDraft";
324
+ readonly body: string;
325
+ } | {
326
+ readonly _tag: "CancelDraft";
327
+ }, cases: Cases): _$effect_Unify0.Unify<ReturnType<Cases["EnterMode" | "ExitMode" | "MoveSelection" | "DeselectCurrent" | "ToggleCurrent" | "SelectAtCell" | "StartDraft" | "CommitDraft" | "CancelDraft"]>>;
328
+ };
329
+ };
330
+ declare function transition(state: CaptureModeState, intent: CaptureModeIntent): CaptureModeState;
331
+ //#endregion
332
+ //#region src/capture-mode/service.d.ts
333
+ /**
334
+ * Reported by host adapters when forwarding a `CaptureModeDispatchResult.toPersist`
335
+ * Capture to its sink fails or when dispatch itself rejects the draft as invalid.
336
+ */
337
+ type CaptureModeDispatchError = CaptureValidationError | CapturePersistenceError;
338
+ interface CaptureModeDispatchResult {
339
+ readonly state: CaptureModeState;
340
+ readonly toPersist?: Capture;
341
+ }
342
+ declare const CapturePersistence_base: Context.ServiceClass<CapturePersistence, "anscribe/capture-mode/service/CapturePersistence", {
343
+ readonly createCapture: (capture: Capture) => Effect.Effect<void, CapturePersistenceError>;
344
+ }>;
345
+ declare class CapturePersistence extends CapturePersistence_base {}
346
+ declare const CaptureHostFailureReporter_base: Context.ServiceClass<CaptureHostFailureReporter, "anscribe/capture-mode/service/CaptureHostFailureReporter", {
347
+ readonly report: (error: CaptureModeDispatchError) => Effect.Effect<void>;
348
+ }>;
349
+ /**
350
+ * Host adapters route `CaptureModeDispatchError` here so the failure surfaces
351
+ * as a structured log entry. The default reporter logs via `Effect.logError`;
352
+ * tests override with `Layer.succeed(CaptureHostFailureReporter, ...)`.
353
+ */
354
+ declare class CaptureHostFailureReporter extends CaptureHostFailureReporter_base {
355
+ static readonly live: Layer.Layer<CaptureHostFailureReporter, never, never>;
356
+ }
357
+ declare const CaptureMode_base: Context.ServiceClass<CaptureMode, "anscribe/capture-mode/service/CaptureMode", {
358
+ readonly dispatch: (intent: CaptureModeIntent) => Effect.Effect<CaptureModeDispatchResult, CaptureValidationError>;
359
+ readonly current: () => Effect.Effect<CaptureModeState>;
360
+ }>;
361
+ declare class CaptureMode extends CaptureMode_base {
362
+ static readonly live: Layer.Layer<CaptureMode, never, never>;
363
+ }
364
+ //#endregion
365
+ //#region src/capture-overlay.d.ts
366
+ declare const ANSCRIBE_OVERLAY: unique symbol;
367
+ declare function markAsOverlay<R extends object>(renderable: R): R;
368
+ declare function isAnscribeOverlay(renderable: unknown): boolean;
369
+ //#endregion
370
+ //#region src/clipboard-format.d.ts
371
+ declare function formatCaptureForClipboard(capture: Capture): string;
372
+ //#endregion
373
+ //#region src/sinks.d.ts
374
+ /**
375
+ * A destination that committed Captures are written to.
376
+ *
377
+ * Host adapters (e.g. `installCapture` in `@anscribe/opentui`) always write
378
+ * every committed Capture to the system clipboard via OSC52 first, then fan
379
+ * out to any user-supplied sinks in order. A sink encapsulates whatever
380
+ * resources its writes need — the host calls `close()` on shutdown so the
381
+ * sink can release them.
382
+ *
383
+ * Sinks are constructed by factories exposed from the package that owns the
384
+ * destination — see `mcpSink()` from `@anscribe/mcp` for the canonical
385
+ * example. Authors of new sinks can implement this interface directly.
386
+ *
387
+ * Sink `write` is allowed to throw or reject; the host wraps the failure in
388
+ * a `CapturePersistenceError` tagged with the sink's `name` and routes it
389
+ * through `CaptureHostFailureReporter`. A failing sink does not roll back
390
+ * earlier sinks' writes or the clipboard handoff.
391
+ */
392
+ interface CaptureSink {
393
+ /**
394
+ * Identifier surfaced in failure messages (e.g. `"anscribe-mcp"`). Should
395
+ * be stable across instances; the React adapter uses the name set to gate
396
+ * `useEffect` reinitialisation when sink instances change between renders.
397
+ */
398
+ readonly name: string;
399
+ /** Persist a Capture. Called once per committed Capture, in array order. */
400
+ readonly write: (capture: Capture) => Promise<void>;
401
+ /** Optional cleanup. Called by the host on `dispose()` / `close()`. */
402
+ readonly close?: () => Promise<void>;
403
+ }
404
+ //#endregion
405
+ export { ANSCRIBE_OVERLAY, Capture, CaptureEnrichmentOutput, CaptureHostFailureReporter, CaptureId, CaptureInput, CaptureMetadata, CaptureMetadataEnricher, CaptureMetadataEnrichment, CaptureMode, CaptureModeDispatchError, CaptureModeDispatchResult, CaptureModeIntent, CaptureModeState, CapturePersistence, CapturePersistenceError, CaptureProjectResolutionError, CaptureSink, CaptureStatus, CaptureValidationError, CapturedTarget, CapturedTargetId, IsoTimestamp, type PickerState, ResolvedCaptureProject, SourceReference, TerminalCellBounds, captureStatusSchema, decodeAnscribeData, decodeAnscribeDataEffect, formatCaptureForClipboard, generateCaptureId, generateCapturedTargetId, initialState, isAnscribeOverlay, makeCapture, markAsOverlay, noCaptureEnrichment, parseIsoTimestampMillis, resolveCaptureProjectBoundary, resolveTargetAtCell, selectCurrentTarget, transition };
package/dist/index.mjs ADDED
@@ -0,0 +1,411 @@
1
+ import { Clock, Context, Data, DateTime, Effect, FileSystem, Layer, Path, Schema, SubscriptionRef } from "effect";
2
+ import { customAlphabet } from "nanoid";
3
+ //#region src/schema.ts
4
+ const CaptureId = Schema.String.pipe(Schema.brand("CaptureId"));
5
+ const CapturedTargetId = Schema.String.pipe(Schema.brand("CapturedTargetId"));
6
+ const generateNanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 12);
7
+ const generateCaptureId = Effect.sync(() => CaptureId.make(generateNanoid()));
8
+ const generateCapturedTargetId = Effect.sync(() => CapturedTargetId.make(generateNanoid()));
9
+ const isIsoTimestamp = Schema.makeFilter((value) => {
10
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && parseIsoTimestampMillis(value) !== void 0 ? true : "Expected ISO timestamp";
11
+ });
12
+ function parseIsoTimestampMillis(value) {
13
+ const millis = Date.parse(value);
14
+ return Number.isFinite(millis) ? millis : void 0;
15
+ }
16
+ const IsoTimestamp = Schema.String.check(isIsoTimestamp).pipe(Schema.brand("IsoTimestamp"));
17
+ var CaptureValidationError = class extends Schema.TaggedErrorClass()("CaptureValidationError", {
18
+ message: Schema.String,
19
+ cause: Schema.optionalKey(Schema.Unknown)
20
+ }) {};
21
+ var CapturePersistenceError = class extends Schema.TaggedErrorClass()("CapturePersistenceError", {
22
+ message: Schema.String,
23
+ cause: Schema.optionalKey(Schema.Unknown)
24
+ }) {};
25
+ var TerminalCellBounds = class extends Schema.Class("TerminalCellBounds")({
26
+ x: Schema.Finite,
27
+ y: Schema.Finite,
28
+ width: Schema.Finite,
29
+ height: Schema.Finite
30
+ }) {};
31
+ var SourceReference = class extends Schema.Class("SourceReference")({
32
+ file: Schema.optionalKey(Schema.String),
33
+ line: Schema.optionalKey(Schema.Finite),
34
+ column: Schema.optionalKey(Schema.Finite),
35
+ functionName: Schema.optionalKey(Schema.String),
36
+ componentName: Schema.optionalKey(Schema.String),
37
+ origin: Schema.optionalKey(Schema.String)
38
+ }) {};
39
+ var CaptureMetadata = class extends Schema.Class("CaptureMetadata")({
40
+ identifier: Schema.optionalKey(Schema.String),
41
+ componentName: Schema.optionalKey(Schema.String),
42
+ componentPath: Schema.optionalKey(Schema.String)
43
+ }) {};
44
+ const captureStatusSchema = Schema.Literals(["pending", "resolved"]);
45
+ var CapturedTarget = class extends Schema.Class("CapturedTarget")({
46
+ id: CapturedTargetId,
47
+ type: Schema.String,
48
+ bounds: TerminalCellBounds,
49
+ ancestry: Schema.Array(Schema.String),
50
+ visibleContent: Schema.optionalKey(Schema.String),
51
+ metadata: Schema.optionalKey(CaptureMetadata),
52
+ sourceReferences: Schema.optionalKey(Schema.Array(SourceReference))
53
+ }) {};
54
+ var Capture = class extends Schema.Class("Capture")({
55
+ id: CaptureId,
56
+ status: captureStatusSchema,
57
+ createdAt: IsoTimestamp,
58
+ instruction: Schema.optionalKey(Schema.String),
59
+ targets: Schema.Array(CapturedTarget)
60
+ }) {};
61
+ const makeCapture = (input) => decodeAnscribeDataEffect(Capture, input);
62
+ function decodeAnscribeData(schema, input) {
63
+ try {
64
+ return Schema.decodeUnknownSync(schema)(input);
65
+ } catch (cause) {
66
+ throw new CaptureValidationError({
67
+ message: "Invalid Anscribe data",
68
+ cause
69
+ });
70
+ }
71
+ }
72
+ const decodeAnscribeDataEffect = (schema, input) => Schema.decodeUnknownEffect(schema)(input).pipe(Effect.mapError((cause) => new CaptureValidationError({
73
+ message: "Invalid Anscribe data",
74
+ cause
75
+ })));
76
+ //#endregion
77
+ //#region src/project.ts
78
+ var CaptureProjectResolutionError = class extends Schema.TaggedErrorClass()("CaptureProjectResolutionError", {
79
+ message: Schema.String,
80
+ cause: Schema.optionalKey(Schema.Unknown)
81
+ }) {};
82
+ var ProjectMarkerNotFoundError = class extends Schema.TaggedErrorClass()("ProjectMarkerNotFoundError", {
83
+ cwd: Schema.String,
84
+ start: Schema.String
85
+ }) {};
86
+ var ProjectPathResolutionError = class extends Schema.TaggedErrorClass()("ProjectPathResolutionError", {
87
+ path: Schema.String,
88
+ cause: Schema.Unknown
89
+ }) {};
90
+ const workspaceMarkers = [
91
+ "pnpm-workspace.yaml",
92
+ "bun.lock",
93
+ "package-lock.json",
94
+ "yarn.lock",
95
+ "package.json"
96
+ ];
97
+ const projectMarkerNames = [".git", ...workspaceMarkers];
98
+ const resolveCaptureProjectBoundary = (cwd) => resolveCaptureProjectRoot(cwd).pipe(Effect.map((projectRoot) => ({ projectRoot })), Effect.mapError(toCaptureProjectResolutionError));
99
+ const resolveCaptureProjectRoot = Effect.fn("Project.resolveCaptureProjectRoot")(function* (cwd) {
100
+ const fs = yield* FileSystem.FileSystem;
101
+ const start = yield* Effect.mapError(fs.realPath(cwd), (cause) => new ProjectPathResolutionError({
102
+ path: cwd,
103
+ cause
104
+ }));
105
+ const projectRoot = yield* findProjectRoot(start);
106
+ if (projectRoot === void 0) return yield* new ProjectMarkerNotFoundError({
107
+ cwd,
108
+ start
109
+ });
110
+ return projectRoot;
111
+ });
112
+ const findProjectRoot = Effect.fn("Project.findProjectRoot")(function* (start) {
113
+ const path = yield* Path.Path;
114
+ let current = start;
115
+ while (true) {
116
+ if (yield* hasProjectMarker(current)) return current;
117
+ const parent = path.dirname(current);
118
+ if (parent === current) return;
119
+ current = parent;
120
+ }
121
+ });
122
+ const hasProjectMarker = Effect.fn("Project.hasProjectMarker")(function* (directory) {
123
+ const path = yield* Path.Path;
124
+ if (yield* markerExists(path.join(directory, ".git"))) return true;
125
+ for (const marker of workspaceMarkers) if (yield* markerExists(path.join(directory, marker))) return true;
126
+ return false;
127
+ });
128
+ const markerExists = Effect.fn("Project.markerExists")(function* (path) {
129
+ const fs = yield* FileSystem.FileSystem;
130
+ return yield* Effect.mapError(fs.exists(path), (cause) => new ProjectPathResolutionError({
131
+ path,
132
+ cause
133
+ }));
134
+ });
135
+ function toCaptureProjectResolutionError(error) {
136
+ if (Schema.is(CaptureProjectResolutionError)(error)) return error;
137
+ if (Schema.is(ProjectMarkerNotFoundError)(error)) return new CaptureProjectResolutionError({
138
+ message: `No Anscribe project markers found from ${error.start}. Add a .git directory/file or package workspace marker (${projectMarkerNames.join(", ")}).`,
139
+ cause: error
140
+ });
141
+ if (Schema.is(ProjectPathResolutionError)(error)) return new CaptureProjectResolutionError({
142
+ message: `Unable to resolve Anscribe project path ${error.path}.`,
143
+ cause: error.cause
144
+ });
145
+ return new CaptureProjectResolutionError({
146
+ message: "Unable to resolve Anscribe project path.",
147
+ cause: error
148
+ });
149
+ }
150
+ //#endregion
151
+ //#region src/enrichment.ts
152
+ const noCaptureEnrichment = Effect.succeed(void 0);
153
+ var CaptureMetadataEnrichment = class extends Context.Service()("anscribe/enrichment/CaptureMetadataEnrichment") {
154
+ static live = Layer.succeed(this, this.of({ enrich: () => noCaptureEnrichment }));
155
+ static layer = (enricher) => Layer.succeed(this, this.of({ enrich: enricher }));
156
+ };
157
+ //#endregion
158
+ //#region src/picker/state.ts
159
+ const PickerIntent = Data.taggedEnum();
160
+ const PICKER_INTENT_TAGS = new Set([
161
+ "EnterMode",
162
+ "ExitMode",
163
+ "MoveSelection",
164
+ "DeselectCurrent",
165
+ "ToggleCurrent",
166
+ "SelectAtCell"
167
+ ]);
168
+ function isPickerIntent(intent) {
169
+ return PICKER_INTENT_TAGS.has(intent._tag);
170
+ }
171
+ function pickerTransition(state, intent) {
172
+ return PickerIntent.$match(intent, {
173
+ EnterMode: ({ targets }) => ({
174
+ active: true,
175
+ targets,
176
+ currentIndex: -1,
177
+ selectedTargetIds: []
178
+ }),
179
+ ExitMode: () => ({
180
+ active: false,
181
+ targets: state.targets,
182
+ currentIndex: -1,
183
+ selectedTargetIds: []
184
+ }),
185
+ MoveSelection: ({ direction }) => {
186
+ if (!state.active || state.targets.length === 0) return state;
187
+ const length = state.targets.length;
188
+ const nextIndex = computeNextIndex(state.currentIndex, length, direction);
189
+ return {
190
+ ...state,
191
+ currentIndex: nextIndex
192
+ };
193
+ },
194
+ DeselectCurrent: () => {
195
+ const currentTarget = selectCurrentTarget(state);
196
+ if (currentTarget === void 0) return state;
197
+ return {
198
+ ...state,
199
+ selectedTargetIds: removeSelection(state.selectedTargetIds, currentTarget.id)
200
+ };
201
+ },
202
+ ToggleCurrent: () => {
203
+ const currentTarget = selectCurrentTarget(state);
204
+ if (currentTarget === void 0) return state;
205
+ return {
206
+ ...state,
207
+ selectedTargetIds: state.selectedTargetIds.includes(currentTarget.id) ? removeSelection(state.selectedTargetIds, currentTarget.id) : addSelection(state.selectedTargetIds, currentTarget.id)
208
+ };
209
+ },
210
+ SelectAtCell: ({ x, y }) => {
211
+ if (!state.active) return state;
212
+ const target = resolveTargetAtCell(state.targets, x, y);
213
+ if (target === void 0) return state;
214
+ const nextIndex = state.targets.findIndex((candidate) => candidate.id === target.id);
215
+ return {
216
+ ...state,
217
+ currentIndex: nextIndex,
218
+ selectedTargetIds: state.selectedTargetIds.includes(target.id) ? removeSelection(state.selectedTargetIds, target.id) : addSelection(state.selectedTargetIds, target.id)
219
+ };
220
+ }
221
+ });
222
+ }
223
+ function selectCurrentTarget(state) {
224
+ return state.active && state.currentIndex >= 0 ? state.targets[state.currentIndex] : void 0;
225
+ }
226
+ function selectSelectedTargets(state) {
227
+ if (state.selectedTargetIds.length > 0) return state.selectedTargetIds.map((id) => state.targets.find((target) => target.id === id)).filter((target) => target !== void 0);
228
+ const currentTarget = selectCurrentTarget(state);
229
+ return currentTarget !== void 0 ? [currentTarget] : [];
230
+ }
231
+ function resolveTargetAtCell(targets, x, y) {
232
+ return targets.filter((target) => containsCell(target.bounds, x, y)).toSorted((a, b) => b.ancestry.length - a.ancestry.length)[0];
233
+ }
234
+ function computeNextIndex(currentIndex, length, direction) {
235
+ if (currentIndex < 0) return direction === "next" ? 0 : length - 1;
236
+ return direction === "next" ? (currentIndex + 1) % length : (currentIndex - 1 + length) % length;
237
+ }
238
+ function containsCell(bounds, x, y) {
239
+ return x >= bounds.x && x < bounds.x + bounds.width && y >= bounds.y && y < bounds.y + bounds.height;
240
+ }
241
+ function addSelection(existing, id) {
242
+ return existing.includes(id) ? existing : [...existing, id];
243
+ }
244
+ function removeSelection(existing, id) {
245
+ const next = existing.filter((candidate) => candidate !== id);
246
+ return next.length === existing.length ? existing : next;
247
+ }
248
+ //#endregion
249
+ //#region src/capture-mode/state.ts
250
+ const initialState = {
251
+ active: false,
252
+ targets: [],
253
+ currentIndex: -1,
254
+ selectedTargetIds: [],
255
+ instructionDraft: false
256
+ };
257
+ const CaptureModeIntent = Data.taggedEnum();
258
+ function transition(state, intent) {
259
+ if (isPickerIntent(intent)) {
260
+ const next = pickerTransition(state, intent);
261
+ if (intent._tag === "EnterMode" || intent._tag === "ExitMode") return {
262
+ ...next,
263
+ instructionDraft: false
264
+ };
265
+ return next;
266
+ }
267
+ return draftTransition(state, intent);
268
+ }
269
+ function draftTransition(state, intent) {
270
+ switch (intent._tag) {
271
+ case "StartDraft":
272
+ if (selectCurrentTarget(state) === void 0 && state.selectedTargetIds.length === 0) return state;
273
+ return {
274
+ ...state,
275
+ instructionDraft: true
276
+ };
277
+ case "CommitDraft": {
278
+ if (!state.instructionDraft) return state;
279
+ const hasExplicitSelection = state.selectedTargetIds.length > 0;
280
+ const willPersist = intent.body.trim().length > 0 && hasResolvedTargets(state);
281
+ return {
282
+ ...state,
283
+ instructionDraft: false,
284
+ selectedTargetIds: willPersist && hasExplicitSelection ? [] : state.selectedTargetIds
285
+ };
286
+ }
287
+ case "CancelDraft":
288
+ if (!state.instructionDraft) return state;
289
+ return {
290
+ ...state,
291
+ instructionDraft: false
292
+ };
293
+ }
294
+ }
295
+ function hasResolvedTargets(state) {
296
+ if (state.selectedTargetIds.length > 0) return true;
297
+ return selectCurrentTarget(state) !== void 0;
298
+ }
299
+ //#endregion
300
+ //#region src/capture-mode/service.ts
301
+ var CapturePersistence = class extends Context.Service()("anscribe/capture-mode/service/CapturePersistence") {};
302
+ /**
303
+ * Host adapters route `CaptureModeDispatchError` here so the failure surfaces
304
+ * as a structured log entry. The default reporter logs via `Effect.logError`;
305
+ * tests override with `Layer.succeed(CaptureHostFailureReporter, ...)`.
306
+ */
307
+ var CaptureHostFailureReporter = class extends Context.Service()("anscribe/capture-mode/service/CaptureHostFailureReporter") {
308
+ static live = Layer.succeed(this, this.of({ report: Effect.fn("CaptureHostFailureReporter.report")((error) => Effect.logError("Unable to persist Anscribe Capture", error)) }));
309
+ };
310
+ var CaptureMode = class CaptureMode extends Context.Service()("anscribe/capture-mode/service/CaptureMode") {
311
+ static live = Layer.effect(CaptureMode, Effect.gen(function* () {
312
+ const ref = yield* SubscriptionRef.make(initialState);
313
+ return CaptureMode.of({
314
+ dispatch: Effect.fn("CaptureMode.dispatch")(function* (intent) {
315
+ if (intent._tag === "CommitDraft") {
316
+ const previousState = yield* SubscriptionRef.get(ref);
317
+ const state = yield* SubscriptionRef.updateAndGet(ref, (current) => transition(current, intent));
318
+ const toPersist = yield* makeCommitDraftCapture(previousState, intent.body);
319
+ return toPersist !== void 0 ? {
320
+ state,
321
+ toPersist
322
+ } : { state };
323
+ }
324
+ return { state: yield* SubscriptionRef.updateAndGet(ref, (current) => transition(current, intent)) };
325
+ }),
326
+ current: () => SubscriptionRef.get(ref)
327
+ });
328
+ }));
329
+ };
330
+ const makeCommitDraftCapture = (state, body) => Effect.gen(function* () {
331
+ const trimmedBody = body.trim();
332
+ if (!(!state.instructionDraft || trimmedBody.length > 0)) return;
333
+ const selectedTargets = selectSelectedTargets(state);
334
+ if (selectedTargets.length === 0) return;
335
+ const instruction = trimmedBody.length > 0 ? trimmedBody : void 0;
336
+ const millis = yield* Clock.currentTimeMillis;
337
+ const createdAt = DateTime.formatIso(DateTime.makeUnsafe(millis));
338
+ const targets = yield* Effect.forEach(selectedTargets, (target) => Effect.gen(function* () {
339
+ const id = yield* generateCapturedTargetId;
340
+ return yield* decodeAnscribeDataEffect(CapturedTarget, {
341
+ ...target,
342
+ id
343
+ });
344
+ }));
345
+ return yield* makeCapture({
346
+ id: yield* generateCaptureId,
347
+ createdAt: IsoTimestamp.make(createdAt),
348
+ status: "pending",
349
+ ...instruction !== void 0 && { instruction },
350
+ targets
351
+ });
352
+ });
353
+ //#endregion
354
+ //#region src/capture-overlay.ts
355
+ const ANSCRIBE_OVERLAY = Symbol.for("anscribe.captureOverlay");
356
+ function markAsOverlay(renderable) {
357
+ Object.defineProperty(renderable, ANSCRIBE_OVERLAY, {
358
+ value: true,
359
+ enumerable: false,
360
+ configurable: false,
361
+ writable: false
362
+ });
363
+ return renderable;
364
+ }
365
+ function isAnscribeOverlay(renderable) {
366
+ return renderable !== null && typeof renderable === "object" && renderable[ANSCRIBE_OVERLAY] === true;
367
+ }
368
+ //#endregion
369
+ //#region src/clipboard-format.ts
370
+ const VISIBLE_CONTENT_PREVIEW_LIMIT = 200;
371
+ function formatCaptureForClipboard(capture) {
372
+ const blocks = [];
373
+ if (capture.instruction !== void 0 && capture.instruction.length > 0) blocks.push(capture.instruction);
374
+ for (const target of capture.targets) blocks.push(formatTarget(target));
375
+ return blocks.join("\n\n");
376
+ }
377
+ function formatTarget(target) {
378
+ return [formatTargetHeader(target), ...formatTargetAncestry(target)].join("\n");
379
+ }
380
+ function formatTargetHeader(target) {
381
+ const identifier = target.metadata?.identifier;
382
+ const idAttr = identifier !== void 0 ? ` id="${identifier}"` : "";
383
+ const preview = formatVisibleContent(target.visibleContent);
384
+ const suffix = preview !== void 0 ? ` ${preview}` : "";
385
+ return `<${target.type}${idAttr}>${suffix}`;
386
+ }
387
+ function formatVisibleContent(content) {
388
+ if (content === void 0) return void 0;
389
+ const collapsed = content.replace(/\s+/g, " ").trim();
390
+ if (collapsed.length === 0) return void 0;
391
+ return `"${collapsed.length > VISIBLE_CONTENT_PREVIEW_LIMIT ? `${collapsed.slice(0, VISIBLE_CONTENT_PREVIEW_LIMIT - 1)}…` : collapsed}"`;
392
+ }
393
+ function formatTargetAncestry(target) {
394
+ const sourceReferences = target.sourceReferences ?? [];
395
+ if (sourceReferences.length > 0) return sourceReferences.map(formatSourceReferenceLine).filter((line) => line !== void 0);
396
+ const componentPath = target.metadata?.componentPath;
397
+ if (componentPath !== void 0 && componentPath.length > 0) return componentPath.split(" > ").map((name) => name.trim()).filter((name) => name.length > 0).map((name) => ` in ${name}`);
398
+ return [];
399
+ }
400
+ function formatSourceReferenceLine(reference) {
401
+ const name = reference.componentName ?? "<anonymous>";
402
+ const location = formatSourceLocation(reference.file, reference.line);
403
+ if (location === void 0 && reference.componentName === void 0) return;
404
+ return location !== void 0 ? ` in ${name} (at ${location})` : ` in ${name}`;
405
+ }
406
+ function formatSourceLocation(file, line) {
407
+ if (file === void 0) return void 0;
408
+ return line !== void 0 ? `${file}:${line}` : file;
409
+ }
410
+ //#endregion
411
+ export { ANSCRIBE_OVERLAY, Capture, CaptureHostFailureReporter, CaptureId, CaptureMetadata, CaptureMetadataEnrichment, CaptureMode, CaptureModeIntent, CapturePersistence, CapturePersistenceError, CaptureProjectResolutionError, CaptureValidationError, CapturedTarget, CapturedTargetId, IsoTimestamp, SourceReference, TerminalCellBounds, captureStatusSchema, decodeAnscribeData, decodeAnscribeDataEffect, formatCaptureForClipboard, generateCaptureId, generateCapturedTargetId, initialState, isAnscribeOverlay, makeCapture, markAsOverlay, noCaptureEnrichment, parseIsoTimestampMillis, resolveCaptureProjectBoundary, resolveTargetAtCell, selectCurrentTarget, transition };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@anscribe/core",
3
+ "version": "0.1.0",
4
+ "description": "Anscribe core: OpenTUI-agnostic capture model, project discovery, and clipboard formatting.",
5
+ "keywords": [
6
+ "agent",
7
+ "anscribe",
8
+ "capture",
9
+ "opentui",
10
+ "tui"
11
+ ],
12
+ "homepage": "https://github.com/msmps/anscribe",
13
+ "bugs": {
14
+ "url": "https://github.com/msmps/anscribe/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": "msmps",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/msmps/anscribe.git",
21
+ "directory": "packages/core"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "type": "module",
27
+ "exports": {
28
+ "./package.json": "./package.json",
29
+ ".": {
30
+ "types": "./dist/index.d.mts",
31
+ "import": "./dist/index.mjs"
32
+ }
33
+ },
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "provenance": true,
37
+ "registry": "https://registry.npmjs.org"
38
+ },
39
+ "dependencies": {
40
+ "effect": "^4.0.0-beta.65",
41
+ "nanoid": "^5.1.6"
42
+ },
43
+ "devDependencies": {
44
+ "@effect/platform-node": "^4.0.0-beta.65"
45
+ },
46
+ "scripts": {
47
+ "build": "tsdown",
48
+ "test": "vitest run"
49
+ }
50
+ }