@jini-ai/vibecoding 0.4.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +154 -0
  3. package/dist/core/apply.d.ts +69 -0
  4. package/dist/core/apply.js +69 -0
  5. package/dist/core/history.d.ts +144 -0
  6. package/dist/core/history.js +132 -0
  7. package/dist/core/index.d.ts +19 -0
  8. package/dist/core/index.js +3 -0
  9. package/dist/core/target.d.ts +92 -0
  10. package/dist/core/target.js +2 -0
  11. package/dist/core/types.d.ts +86 -0
  12. package/dist/core/types.js +19 -0
  13. package/dist/html/index.d.ts +12 -0
  14. package/dist/html/index.js +2 -0
  15. package/dist/html/node/index.d.ts +10 -0
  16. package/dist/html/node/index.js +10 -0
  17. package/dist/html/node/parse5-region-parser.d.ts +12 -0
  18. package/dist/html/node/parse5-region-parser.js +213 -0
  19. package/dist/html/regions.d.ts +137 -0
  20. package/dist/html/regions.js +165 -0
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.js +10 -0
  23. package/dist/react/components/DocumentPreview.d.ts +59 -0
  24. package/dist/react/components/DocumentPreview.js +44 -0
  25. package/dist/react/components/PartsViewer.d.ts +49 -0
  26. package/dist/react/components/PartsViewer.js +61 -0
  27. package/dist/react/components/VibecodingWorkbench.d.ts +38 -0
  28. package/dist/react/components/VibecodingWorkbench.js +96 -0
  29. package/dist/react/index.d.ts +29 -0
  30. package/dist/react/index.js +7 -0
  31. package/dist/react/session.d.ts +108 -0
  32. package/dist/react/session.js +160 -0
  33. package/dist/react/tools.d.ts +91 -0
  34. package/dist/react/tools.js +246 -0
  35. package/dist/react/use-vibecoding-session.d.ts +30 -0
  36. package/dist/react/use-vibecoding-session.js +44 -0
  37. package/package.json +99 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @module react/session
3
+ *
4
+ * `VibecodingSession` — the one stateful object this package's React layer adds on top of
5
+ * `../core`'s `EditTarget` + `EditHistory`. Everything else in `./react` (the hook, the tool
6
+ * registrations, the presentational components) reads and drives an artifact through this seam
7
+ * rather than touching `EditTarget`/`EditHistory` directly, for one reason: a tool call arrives
8
+ * from the chat's execution path, not from a React event handler, so the thing tool handlers call
9
+ * into cannot be React state — it has to be a plain, subscribable object a `useSyncExternalStore`
10
+ * hook can also read. This is the same shape React itself recommends for external stores; nothing
11
+ * here is React-specific, but it lives under `./react` because "chat + preview surface" is exactly
12
+ * what it exists to support (see this package's README's `./react` row).
13
+ *
14
+ * ## Why part content is cached rather than re-read on every render
15
+ *
16
+ * `EditTarget.readPart` is a host call — for the `html` target it is cheap (a string slice), but
17
+ * the port makes no such promise for every target (a filesystem or sandbox-backed host could hit a
18
+ * network round trip per part). Re-reading every listed part's content on every `refresh()` would
19
+ * turn "the operator opened the file viewer" into an O(part count) fan-out of host calls, and
20
+ * nothing here can assume that count stays small — see `../core/target.ts`'s own doc on why
21
+ * `listParts` is the allowlist, not a bound on its size. So `refresh()` only calls `listParts()`;
22
+ * content is fetched lazily, per id, through `readPart()`, and cached. Every mutating operation
23
+ * below (`applyEdits`, `undo`, `redo`, `restoreSnapshot`) already knows the exact before/after
24
+ * content it just wrote — `../core/apply.ts`'s `ProposedEdit`, `../core/history.ts`'s
25
+ * `PartChange.before`/`.after` — so the cache is updated from that data directly instead of
26
+ * re-reading the part it just wrote back from the host.
27
+ */
28
+ import type { ApplyOutcome, PartId, PartRef, Snapshot } from '../core/types.js';
29
+ import type { EditTarget } from '../core/target.js';
30
+ import { type ProposedEdit } from '../core/apply.js';
31
+ import { type EditHistory, type EditHistoryOptions, type HistoryEntry } from '../core/history.js';
32
+ export interface VibecodingSessionArgs {
33
+ /** The host's artifact port. See `../core/target.ts`. */
34
+ readonly target: EditTarget;
35
+ }
36
+ export interface VibecodingSessionOptions {
37
+ /** Forwarded to `createEditHistory`. See `../core/history.ts`'s `EditHistoryOptions`. */
38
+ readonly historyOptions?: EditHistoryOptions;
39
+ }
40
+ /**
41
+ * Immutable, point-in-time read of a session. `getSnapshot()` returns the same object reference
42
+ * until something actually changes — required for `useSyncExternalStore`, which re-renders a
43
+ * subscriber whenever the reference it reads changes, not whenever it is merely called again.
44
+ */
45
+ export interface VibecodingSessionSnapshot {
46
+ /** `'idle'` until the first `refresh()` resolves; `'error'` reflects the most recent failure, not
47
+ * a permanently wedged session — a subsequent successful call returns it to `'ready'`. */
48
+ readonly status: 'idle' | 'loading' | 'ready' | 'error';
49
+ /** Every part the host currently advertises, in `EditTarget.listParts()`'s own order. */
50
+ readonly parts: readonly PartRef[];
51
+ /** Cached content, keyed by part id. A part absent from this map has not been read yet — see
52
+ * this module's doc for why content is lazy rather than eagerly fetched for every part. */
53
+ readonly partContent: ReadonlyMap<PartId, string>;
54
+ readonly canUndo: boolean;
55
+ readonly canRedo: boolean;
56
+ /** The most recent operation's failure message, if any. Cleared by the next successful call. */
57
+ readonly lastError: string | undefined;
58
+ }
59
+ export interface ApplyEditsResult {
60
+ readonly outcomes: readonly ApplyOutcome[];
61
+ /** See `../core/apply.ts`'s `correctionsFor` — the subset a caller feeds back to the model. */
62
+ readonly corrections: readonly {
63
+ readonly id: PartId;
64
+ readonly reason: string;
65
+ }[];
66
+ }
67
+ export interface VibecodingSession {
68
+ /** The underlying port, for a caller (or test) that needs it directly — e.g. to read the whole
69
+ * document a host-specific target stores, which is outside this generic session's vocabulary. */
70
+ readonly target: EditTarget;
71
+ /** The underlying undo/redo stack, for a caller that needs `.entries()` for a history UI. */
72
+ readonly history: EditHistory;
73
+ getSnapshot(): VibecodingSessionSnapshot;
74
+ /** Returns an unsubscribe function; the React hook adapts this object API for React. */
75
+ subscribe(args: {
76
+ readonly listener: () => void;
77
+ }): () => void;
78
+ /** Re-lists parts from the host. Does not re-read content — see this module's doc. */
79
+ refresh(): Promise<void>;
80
+ /** Cached content for `id`, fetching once from the host on first request. */
81
+ readPart(args: {
82
+ readonly id: PartId;
83
+ }): Promise<string>;
84
+ /** Applies `edits` as one undoable transaction and updates the cache from the edits themselves. */
85
+ applyEdits(requiredArgs: {
86
+ readonly edits: readonly ProposedEdit[];
87
+ }, optionalArgs?: {
88
+ readonly label?: string;
89
+ }): Promise<ApplyEditsResult>;
90
+ undo(): Promise<HistoryEntry | null>;
91
+ redo(): Promise<HistoryEntry | null>;
92
+ takeSnapshot(): Promise<Snapshot>;
93
+ restoreSnapshot(args: {
94
+ readonly snapshot: Snapshot;
95
+ }): Promise<HistoryEntry | null>;
96
+ }
97
+ /**
98
+ * Builds a session over the required `target` port, with its own private `EditHistory`.
99
+ * History settings belong to the optional second object.
100
+ *
101
+ * The returned session starts in `'idle'` with no parts listed — call `refresh()` (or let
102
+ * `useVibecodingSession` do it on mount) to populate it. Nothing here triggers I/O eagerly, so
103
+ * constructing a session has no side effect a caller needs to clean up.
104
+ *
105
+ * @complexity Construction is O(1). See each method for its own cost.
106
+ */
107
+ export declare function createVibecodingSession({ target }: VibecodingSessionArgs, options?: VibecodingSessionOptions): VibecodingSession;
108
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1,160 @@
1
+ import { applyEdits as applyEditsToTarget, correctionsFor } from '../core/apply.js';
2
+ import { createEditHistory } from '../core/history.js';
3
+ const READY_ERROR = undefined;
4
+ /**
5
+ * Builds a session over the required `target` port, with its own private `EditHistory`.
6
+ * History settings belong to the optional second object.
7
+ *
8
+ * The returned session starts in `'idle'` with no parts listed — call `refresh()` (or let
9
+ * `useVibecodingSession` do it on mount) to populate it. Nothing here triggers I/O eagerly, so
10
+ * constructing a session has no side effect a caller needs to clean up.
11
+ *
12
+ * @complexity Construction is O(1). See each method for its own cost.
13
+ */
14
+ export function createVibecodingSession({ target }, options = {}) {
15
+ const history = createEditHistory({ target }, options.historyOptions);
16
+ const listeners = new Set();
17
+ const partContent = new Map();
18
+ let current = {
19
+ status: 'idle',
20
+ parts: [],
21
+ partContent,
22
+ canUndo: history.canUndo(),
23
+ canRedo: history.canRedo(),
24
+ lastError: READY_ERROR,
25
+ };
26
+ /** Replaces `current` with a fresh object carrying `patch`, then notifies subscribers. A fresh
27
+ * `partContent` Map reference is handed out only when the map's contents actually changed
28
+ * (callers pass a new Map in `patch` for that case); reusing the same Map reference otherwise
29
+ * is what keeps an unrelated status change (e.g. `'loading'` -> `'ready'`) from invalidating a
30
+ * component's memoized read of `partContent`. */
31
+ function commit(patch) {
32
+ current = { ...current, ...patch };
33
+ for (const listener of listeners)
34
+ listener();
35
+ }
36
+ /**
37
+ * Runs `work`, tagging the session `'loading'` -> `'ready'`/`'error'` and always returning
38
+ * control to the caller — a rejection is recorded as `lastError` AND rethrown, so a component
39
+ * can render `lastError` passively while a tool handler that needs to fail loudly still can.
40
+ *
41
+ * `work` returns its data `patch` alongside its `result` rather than calling `commit` itself, so
42
+ * the success path here can fold that patch into the SAME commit as the `'ready'` status
43
+ * transition — one notification per operation instead of two, which matters to a
44
+ * `useSyncExternalStore` subscriber re-rendering on every commit.
45
+ */
46
+ async function run(work) {
47
+ commit({ status: 'loading' });
48
+ try {
49
+ const { result, patch } = await work();
50
+ commit({ ...patch, status: 'ready', lastError: READY_ERROR });
51
+ return result;
52
+ }
53
+ catch (thrown) {
54
+ const message = thrown instanceof Error ? thrown.message : String(thrown);
55
+ commit({ status: 'error', lastError: message });
56
+ throw thrown;
57
+ }
58
+ }
59
+ /** Mirrors a replayed change in the cache. Undoing creation keeps empty content because the
60
+ * target's upsert port has no delete operation; see `../core/history.ts`. */
61
+ function cacheChange(change, side) {
62
+ partContent.set(change.id, side === 'after' ? change.after : change.before);
63
+ }
64
+ return {
65
+ target,
66
+ history,
67
+ getSnapshot: () => current,
68
+ subscribe({ listener }) {
69
+ listeners.add(listener);
70
+ return () => listeners.delete(listener);
71
+ },
72
+ refresh: () => run(async () => {
73
+ const parts = await target.listParts();
74
+ const known = new Set(parts.map((p) => p.id));
75
+ // Drop cached content for parts the host no longer lists — stale content for an
76
+ // unaddressable id would otherwise linger forever with nothing to invalidate it.
77
+ for (const id of partContent.keys())
78
+ if (!known.has(id))
79
+ partContent.delete(id);
80
+ return {
81
+ result: undefined,
82
+ patch: { parts, partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() },
83
+ };
84
+ }),
85
+ readPart: ({ id }) => run(async () => {
86
+ const cached = partContent.get(id);
87
+ if (cached !== undefined)
88
+ return { result: cached, patch: {} };
89
+ const content = await target.readPart({ id });
90
+ partContent.set(id, content);
91
+ return { result: content, patch: { partContent: new Map(partContent) } };
92
+ }),
93
+ applyEdits: ({ edits }, options = {}) => run(async () => {
94
+ const outcomes = await history.transaction({ work: (recording) => applyEditsToTarget({ target: recording, edits }) }, options);
95
+ // Outcomes are aligned with proposals, including repeated ids. Matching only by id
96
+ // would cache the first proposal instead of the last successful write for that part.
97
+ // This pass is O(edits.length), with no additional reads through the target port.
98
+ for (const [index, outcome] of outcomes.entries()) {
99
+ if (outcome.status !== 'applied')
100
+ continue;
101
+ const edit = edits[index];
102
+ if (edit)
103
+ partContent.set(edit.id, edit.content);
104
+ }
105
+ const parts = await target.listParts();
106
+ return {
107
+ result: { outcomes, corrections: correctionsFor({ outcomes }) },
108
+ patch: {
109
+ parts,
110
+ partContent: new Map(partContent),
111
+ canUndo: history.canUndo(),
112
+ canRedo: history.canRedo(),
113
+ },
114
+ };
115
+ }),
116
+ undo: () => run(async () => {
117
+ const entry = await history.undo();
118
+ if (!entry)
119
+ return { result: entry, patch: {} };
120
+ // Undo replays in reverse order. Repeated ids must leave the earliest before value
121
+ // cached, matching what history wrote to the target.
122
+ for (const change of [...entry.changes].reverse())
123
+ cacheChange(change, 'before');
124
+ return {
125
+ result: entry,
126
+ patch: { partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() },
127
+ };
128
+ }),
129
+ redo: () => run(async () => {
130
+ const entry = await history.redo();
131
+ if (!entry)
132
+ return { result: entry, patch: {} };
133
+ for (const change of entry.changes)
134
+ cacheChange(change, 'after');
135
+ return {
136
+ result: entry,
137
+ patch: { partContent: new Map(partContent), canUndo: history.canUndo(), canRedo: history.canRedo() },
138
+ };
139
+ }),
140
+ takeSnapshot: () => run(async () => ({ result: await target.snapshot(), patch: {} })),
141
+ restoreSnapshot: ({ snapshot }) => run(async () => {
142
+ const entry = await history.restore({ snapshot });
143
+ if (!entry)
144
+ return { result: entry, patch: {} };
145
+ for (const change of entry.changes)
146
+ cacheChange(change, 'after');
147
+ const parts = await target.listParts();
148
+ return {
149
+ result: entry,
150
+ patch: {
151
+ parts,
152
+ partContent: new Map(partContent),
153
+ canUndo: history.canUndo(),
154
+ canRedo: history.canRedo(),
155
+ },
156
+ };
157
+ }),
158
+ };
159
+ }
160
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @module react/tools
3
+ *
4
+ * Turns one `VibecodingSession` (`./session.js`) into the chat-callable control surface the user's
5
+ * vision describes: "tools in the vibecoding package that let us control everything from chat."
6
+ * Every tool here is declared as an `@jini-ai/core` `ToolDescriptor` — the kernel's own
7
+ * discoverable-tool shape (`id`, `description`, `inputSchema`, `readOnly`, ...) — so a host wires
8
+ * these into a real `ToolRegistry` and a chat surface enumerates them via `registry.list({})` rather
9
+ * than importing a hardcoded list of vibecoding-specific function names.
10
+ *
11
+ * ## Two ways to run these tools, because `@jini-ai/core`'s registry deliberately hides handlers
12
+ *
13
+ * `ToolRegistry` (`@jini-ai/core`) never lets a caller retrieve a registered handler directly —
14
+ * only `authorizeToolInvocation` can, and that function is exported from `@jini-ai/core/composition`
15
+ * for `@jini-ai/daemon`'s `ToolExecutor` alone (see that file's own module doc: "the one and only
16
+ * intended caller"). That is the right boundary for a real backend, but it means a browser host
17
+ * with no daemon in front of it — the common case for `./react`, a chat surface running client-side
18
+ * — has no way to actually invoke a tool it just registered.
19
+ *
20
+ * So this module exposes two views over the same underlying definitions:
21
+ *
22
+ * - {@link createVibecodingToolRegistrations} — real `ToolRegistration`s, ready for
23
+ * `registry.register(...)` on any `@jini-ai/core` `ToolRegistry`. This is what makes the tools
24
+ * genuinely discoverable and, once a real daemon sits in front of this session, genuinely
25
+ * executable through `ToolExecutor`'s authorized path.
26
+ * - {@link createVibecodingToolRunner} — the same tool ids and descriptors, plus a `run({ toolId, input })`
27
+ * a host without a daemon calls directly. This is not a bypass of the registry's authorization
28
+ * design: these tools have no principal/run/authorization concept of their own to bypass — every
29
+ * one of them acts on a single in-process `VibecodingSession` the host itself already holds, and
30
+ * the actual security boundary (which parts are addressable, and whether a proposed edit may
31
+ * land) is enforced by `EditTarget.validate` inside `session.applyEdits` regardless of which of
32
+ * these two paths reached it. A host that DOES have a daemon and wants centralized authorization
33
+ * registers through the first path instead and never calls `run` at all.
34
+ */
35
+ import { type ToolDescriptor, type ToolPolicy, type ToolRegistration } from '@jini-ai/core';
36
+ import type { VibecodingSession } from './session.js';
37
+ /** Stable tool ids, exported so a host building a catalog (composer menu, MCP tool list, ...) from
38
+ * `ToolRegistry.list({})` can still branch on a known id where it needs to (e.g. to route
39
+ * `propose_edits`'s result into a "corrections" UI) without restating the string literal. */
40
+ export declare const VIBECODING_TOOL_IDS: {
41
+ readonly LIST_PARTS: "vibecoding.list_parts";
42
+ readonly READ_PART: "vibecoding.read_part";
43
+ readonly PROPOSE_EDITS: "vibecoding.propose_edits";
44
+ readonly UNDO: "vibecoding.undo";
45
+ readonly REDO: "vibecoding.redo";
46
+ };
47
+ export interface CreateVibecodingToolRegistrationsOptions {
48
+ /** Overrides {@link ALLOW_ALWAYS} for every registration this call produces. */
49
+ readonly policy?: ToolPolicy;
50
+ }
51
+ /** Required port shared by the registration and direct-runner factories. */
52
+ export interface VibecodingToolArgs {
53
+ readonly session: VibecodingSession;
54
+ }
55
+ /**
56
+ * Builds `ToolRegistration`s ready for `registry.register(...)` on any `@jini-ai/core`
57
+ * `ToolRegistry`. This is the "must register there, not be bespoke" seam: a chat surface backed by
58
+ * a real daemon enumerates these through `registry.list({})` and runs them through
59
+ * `ToolExecutor`/`authorizeToolInvocation`, never through a hardcoded switch on tool name.
60
+ *
61
+ * @param requiredArgs - the artifact session port every tool call acts on.
62
+ * @param options - see {@link CreateVibecodingToolRegistrationsOptions}.
63
+ * @returns one `ToolRegistration` per tool in `VIBECODING_TOOL_IDS`, in that constant's order.
64
+ * @complexity O(1) — five fixed registrations.
65
+ */
66
+ export declare function createVibecodingToolRegistrations({ session }: VibecodingToolArgs, options?: CreateVibecodingToolRegistrationsOptions): ToolRegistration[];
67
+ export interface VibecodingToolRunner {
68
+ /** The same descriptors {@link createVibecodingToolRegistrations} would register — a host with no
69
+ * `ToolRegistry` in play yet (e.g. building a composer catalog directly) reads this instead of
70
+ * standing up a registry purely to call `.list({})` once. */
71
+ readonly descriptors: readonly ToolDescriptor[];
72
+ /** Runs one tool by id directly against the bound session. Rejects with `ToolInputError` for a
73
+ * malformed `input`, with `RangeError` for an unknown `toolId`, or with whatever `session`'s own
74
+ * operation rejects with (see `./session.js`). */
75
+ run(args: {
76
+ readonly toolId: string;
77
+ readonly input: unknown;
78
+ }): Promise<unknown>;
79
+ }
80
+ /**
81
+ * The no-daemon path — see this module's doc for why one is needed at all. Builds one runner over
82
+ * `session` that a browser-only host (no `@jini-ai/daemon` in front of it) can call directly from,
83
+ * e.g., a composer's `onDiscoverySelect` or a mock chat transport.
84
+ *
85
+ * @param requiredArgs - the artifact session port every tool call acts on.
86
+ * @returns descriptors plus a `run` dispatcher — see {@link VibecodingToolRunner}.
87
+ * @complexity `run` is O(n) in tool count (currently 5) to resolve `toolId`, then whatever the
88
+ * resolved handler costs.
89
+ */
90
+ export declare function createVibecodingToolRunner({ session }: VibecodingToolArgs): VibecodingToolRunner;
91
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1,246 @@
1
+ /**
2
+ * @module react/tools
3
+ *
4
+ * Turns one `VibecodingSession` (`./session.js`) into the chat-callable control surface the user's
5
+ * vision describes: "tools in the vibecoding package that let us control everything from chat."
6
+ * Every tool here is declared as an `@jini-ai/core` `ToolDescriptor` — the kernel's own
7
+ * discoverable-tool shape (`id`, `description`, `inputSchema`, `readOnly`, ...) — so a host wires
8
+ * these into a real `ToolRegistry` and a chat surface enumerates them via `registry.list({})` rather
9
+ * than importing a hardcoded list of vibecoding-specific function names.
10
+ *
11
+ * ## Two ways to run these tools, because `@jini-ai/core`'s registry deliberately hides handlers
12
+ *
13
+ * `ToolRegistry` (`@jini-ai/core`) never lets a caller retrieve a registered handler directly —
14
+ * only `authorizeToolInvocation` can, and that function is exported from `@jini-ai/core/composition`
15
+ * for `@jini-ai/daemon`'s `ToolExecutor` alone (see that file's own module doc: "the one and only
16
+ * intended caller"). That is the right boundary for a real backend, but it means a browser host
17
+ * with no daemon in front of it — the common case for `./react`, a chat surface running client-side
18
+ * — has no way to actually invoke a tool it just registered.
19
+ *
20
+ * So this module exposes two views over the same underlying definitions:
21
+ *
22
+ * - {@link createVibecodingToolRegistrations} — real `ToolRegistration`s, ready for
23
+ * `registry.register(...)` on any `@jini-ai/core` `ToolRegistry`. This is what makes the tools
24
+ * genuinely discoverable and, once a real daemon sits in front of this session, genuinely
25
+ * executable through `ToolExecutor`'s authorized path.
26
+ * - {@link createVibecodingToolRunner} — the same tool ids and descriptors, plus a `run({ toolId, input })`
27
+ * a host without a daemon calls directly. This is not a bypass of the registry's authorization
28
+ * design: these tools have no principal/run/authorization concept of their own to bypass — every
29
+ * one of them acts on a single in-process `VibecodingSession` the host itself already holds, and
30
+ * the actual security boundary (which parts are addressable, and whether a proposed edit may
31
+ * land) is enforced by `EditTarget.validate` inside `session.applyEdits` regardless of which of
32
+ * these two paths reached it. A host that DOES have a daemon and wants centralized authorization
33
+ * registers through the first path instead and never calls `run` at all.
34
+ */
35
+ import { ToolInputError, } from '@jini-ai/core';
36
+ /** Stable tool ids, exported so a host building a catalog (composer menu, MCP tool list, ...) from
37
+ * `ToolRegistry.list({})` can still branch on a known id where it needs to (e.g. to route
38
+ * `propose_edits`'s result into a "corrections" UI) without restating the string literal. */
39
+ export const VIBECODING_TOOL_IDS = {
40
+ LIST_PARTS: 'vibecoding.list_parts',
41
+ READ_PART: 'vibecoding.read_part',
42
+ PROPOSE_EDITS: 'vibecoding.propose_edits',
43
+ UNDO: 'vibecoding.undo',
44
+ REDO: 'vibecoding.redo',
45
+ };
46
+ /** A proposed edit in one `vibecoding.propose_edits` call is capped so a single model turn cannot
47
+ * ask this session to rewrite an unbounded number of parts in one transaction — see the
48
+ * Programmer pre-check on service-backed collections this module was written against. Chosen
49
+ * generously above any realistic single-turn edit count rather than tuned to one; a host needing
50
+ * fewer or more can still bound its own model prompt independently. */
51
+ const MAX_EDITS_PER_INVOCATION = 50;
52
+ /** No-argument tools still declare an (empty) object schema rather than omitting `inputSchema`, so
53
+ * a model sees explicitly that `{}` is the correct call shape instead of guessing. */
54
+ const EMPTY_INPUT_SCHEMA = { type: 'object', additionalProperties: false };
55
+ const READ_PART_INPUT_SCHEMA = {
56
+ type: 'object',
57
+ properties: {
58
+ id: { type: 'string', description: 'A part id from a prior vibecoding.list_parts call.' },
59
+ },
60
+ required: ['id'],
61
+ additionalProperties: false,
62
+ };
63
+ const PROPOSE_EDITS_INPUT_SCHEMA = {
64
+ type: 'object',
65
+ properties: {
66
+ edits: {
67
+ type: 'array',
68
+ minItems: 1,
69
+ maxItems: MAX_EDITS_PER_INVOCATION,
70
+ items: {
71
+ type: 'object',
72
+ properties: {
73
+ id: { type: 'string', description: 'A part id from a prior vibecoding.list_parts call.' },
74
+ content: { type: 'string', description: "The part's full replacement content." },
75
+ },
76
+ required: ['id', 'content'],
77
+ additionalProperties: false,
78
+ },
79
+ },
80
+ label: {
81
+ type: 'string',
82
+ description: 'Optional human-facing description of this turn, shown in undo history.',
83
+ },
84
+ },
85
+ required: ['edits'],
86
+ additionalProperties: false,
87
+ };
88
+ /** Narrows `input` to a plain record, the same first check every handler below needs before
89
+ * reading a named field out of a value that arrived as `unknown` model-authored JSON. */
90
+ function requireInputRecord(input, toolId) {
91
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
92
+ throw new ToolInputError({ message: `${toolId}: expected an input object` });
93
+ }
94
+ return input;
95
+ }
96
+ function requireString(record, key, toolId) {
97
+ const value = record[key];
98
+ if (typeof value !== 'string' || value.length === 0) {
99
+ throw new ToolInputError({ message: `${toolId}: "${key}" must be a non-empty string` });
100
+ }
101
+ return value;
102
+ }
103
+ /** Validates the `edits` array's shape without trusting any of it — `content` may legitimately be
104
+ * an empty string (clearing a part), so only `edits` itself and each entry's `id` are non-empty
105
+ * checked. */
106
+ function requireProposedEdits(record, toolId) {
107
+ const raw = record.edits;
108
+ if (!Array.isArray(raw) || raw.length === 0) {
109
+ throw new ToolInputError({ message: `${toolId}: "edits" must be a non-empty array` });
110
+ }
111
+ if (raw.length > MAX_EDITS_PER_INVOCATION) {
112
+ throw new ToolInputError({ message: `${toolId}: "edits" carries ${raw.length} entries, over the ${MAX_EDITS_PER_INVOCATION}-entry limit per call` });
113
+ }
114
+ return raw.map((entry, index) => {
115
+ const entryRecord = requireInputRecord(entry, `${toolId}.edits[${index}]`);
116
+ const id = requireString(entryRecord, 'id', `${toolId}.edits[${index}]`);
117
+ const content = entryRecord.content;
118
+ if (typeof content !== 'string') {
119
+ throw new ToolInputError({ message: `${toolId}.edits[${index}]: "content" must be a string` });
120
+ }
121
+ return { id, content };
122
+ });
123
+ }
124
+ /**
125
+ * Builds the five tool definitions bound to `session`. Package-private: hosts reach these only
126
+ * through {@link createVibecodingToolRegistrations} or {@link createVibecodingToolRunner}, which is
127
+ * what keeps "how a definition is invoked" a single decision made in one of those two places rather
128
+ * than duplicated per tool.
129
+ *
130
+ * @complexity O(1) to build the array; each handler's own cost is documented on its descriptor.
131
+ */
132
+ function buildVibecodingToolDefinitions(session) {
133
+ return [
134
+ {
135
+ descriptor: {
136
+ id: VIBECODING_TOOL_IDS.LIST_PARTS,
137
+ description: 'List every editable part of the artifact currently being authored.',
138
+ inputSchema: EMPTY_INPUT_SCHEMA,
139
+ readOnly: true,
140
+ timeoutMs: 10_000,
141
+ },
142
+ handler: async () => {
143
+ await session.refresh();
144
+ return session.getSnapshot().parts;
145
+ },
146
+ },
147
+ {
148
+ descriptor: {
149
+ id: VIBECODING_TOOL_IDS.READ_PART,
150
+ description: "Read one part's current content by id.",
151
+ inputSchema: READ_PART_INPUT_SCHEMA,
152
+ readOnly: true,
153
+ timeoutMs: 10_000,
154
+ },
155
+ handler: async (input) => {
156
+ const record = requireInputRecord(input, VIBECODING_TOOL_IDS.READ_PART);
157
+ const id = requireString(record, 'id', VIBECODING_TOOL_IDS.READ_PART);
158
+ return { id, content: await session.readPart({ id }) };
159
+ },
160
+ },
161
+ {
162
+ descriptor: {
163
+ id: VIBECODING_TOOL_IDS.PROPOSE_EDITS,
164
+ description: 'Propose replacement content for one or more parts, as a single undoable turn. Each edit ' +
165
+ 'is validated before anything is written; a rejected edit leaves the artifact untouched ' +
166
+ 'and its reason is returned for the next turn.',
167
+ inputSchema: PROPOSE_EDITS_INPUT_SCHEMA,
168
+ readOnly: false,
169
+ timeoutMs: 15_000,
170
+ },
171
+ handler: async (input) => {
172
+ const record = requireInputRecord(input, VIBECODING_TOOL_IDS.PROPOSE_EDITS);
173
+ const edits = requireProposedEdits(record, VIBECODING_TOOL_IDS.PROPOSE_EDITS);
174
+ const label = record.label === undefined ? undefined : requireString(record, 'label', VIBECODING_TOOL_IDS.PROPOSE_EDITS);
175
+ return session.applyEdits({ edits }, label === undefined ? {} : { label });
176
+ },
177
+ },
178
+ {
179
+ descriptor: {
180
+ id: VIBECODING_TOOL_IDS.UNDO,
181
+ description: 'Revert the most recent proposed-edits turn.',
182
+ inputSchema: EMPTY_INPUT_SCHEMA,
183
+ readOnly: false,
184
+ timeoutMs: 10_000,
185
+ },
186
+ handler: async () => ({ entry: await session.undo() }),
187
+ },
188
+ {
189
+ descriptor: {
190
+ id: VIBECODING_TOOL_IDS.REDO,
191
+ description: 'Re-apply the most recently undone turn.',
192
+ inputSchema: EMPTY_INPUT_SCHEMA,
193
+ readOnly: false,
194
+ timeoutMs: 10_000,
195
+ },
196
+ handler: async () => ({ entry: await session.redo() }),
197
+ },
198
+ ];
199
+ }
200
+ /** Grants every call — see this module's doc for why tool-level authorization has nothing to add
201
+ * beyond `EditTarget.validate`'s own allowlist check for a session with no principal concept of
202
+ * its own. A host with per-principal restrictions passes its own `ToolPolicy` via
203
+ * `createVibecodingToolRegistrations`'s `options.policy` instead of relying on this default. */
204
+ const ALLOW_ALWAYS = { authorize: () => 'allow' };
205
+ /**
206
+ * Builds `ToolRegistration`s ready for `registry.register(...)` on any `@jini-ai/core`
207
+ * `ToolRegistry`. This is the "must register there, not be bespoke" seam: a chat surface backed by
208
+ * a real daemon enumerates these through `registry.list({})` and runs them through
209
+ * `ToolExecutor`/`authorizeToolInvocation`, never through a hardcoded switch on tool name.
210
+ *
211
+ * @param requiredArgs - the artifact session port every tool call acts on.
212
+ * @param options - see {@link CreateVibecodingToolRegistrationsOptions}.
213
+ * @returns one `ToolRegistration` per tool in `VIBECODING_TOOL_IDS`, in that constant's order.
214
+ * @complexity O(1) — five fixed registrations.
215
+ */
216
+ export function createVibecodingToolRegistrations({ session }, options = {}) {
217
+ const policy = options.policy ?? ALLOW_ALWAYS;
218
+ return buildVibecodingToolDefinitions(session).map(({ descriptor, handler }) => ({
219
+ descriptor,
220
+ policy,
221
+ handler: (ctx) => handler(ctx.input),
222
+ }));
223
+ }
224
+ /**
225
+ * The no-daemon path — see this module's doc for why one is needed at all. Builds one runner over
226
+ * `session` that a browser-only host (no `@jini-ai/daemon` in front of it) can call directly from,
227
+ * e.g., a composer's `onDiscoverySelect` or a mock chat transport.
228
+ *
229
+ * @param requiredArgs - the artifact session port every tool call acts on.
230
+ * @returns descriptors plus a `run` dispatcher — see {@link VibecodingToolRunner}.
231
+ * @complexity `run` is O(n) in tool count (currently 5) to resolve `toolId`, then whatever the
232
+ * resolved handler costs.
233
+ */
234
+ export function createVibecodingToolRunner({ session }) {
235
+ const definitions = buildVibecodingToolDefinitions(session);
236
+ return {
237
+ descriptors: definitions.map((d) => d.descriptor),
238
+ async run({ toolId, input }) {
239
+ const definition = definitions.find((d) => d.descriptor.id === toolId);
240
+ if (!definition)
241
+ throw new RangeError(`no vibecoding tool registered with id "${toolId}"`);
242
+ return definition.handler(input);
243
+ },
244
+ };
245
+ }
246
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1,30 @@
1
+ import type { VibecodingSession, VibecodingSessionSnapshot } from './session.js';
2
+ export interface UseVibecodingSessionArgs {
3
+ readonly session: VibecodingSession;
4
+ }
5
+ export interface UseVibecodingSessionResult extends VibecodingSessionSnapshot {
6
+ readonly session: VibecodingSession;
7
+ readonly refresh: () => Promise<void>;
8
+ readonly readPart: VibecodingSession['readPart'];
9
+ readonly applyEdits: VibecodingSession['applyEdits'];
10
+ readonly undo: VibecodingSession['undo'];
11
+ readonly redo: VibecodingSession['redo'];
12
+ }
13
+ /**
14
+ * Subscribes to `session` and triggers its first `refresh()` on mount (and again if `session`
15
+ * itself changes identity — a host swapping in a different artifact mid-session gets a fresh list
16
+ * rather than a stale one left over from the previous session). Re-running `refresh()` a second
17
+ * time for the same session instance is the caller's own choice via the returned `refresh`.
18
+ *
19
+ * A `refresh()` rejection is intentionally swallowed here rather than thrown from the effect —
20
+ * `session.getSnapshot().lastError` already carries it (see `./session.js`), and an effect that
21
+ * threw would only produce an uncatchable render-cycle error for a failure the snapshot already
22
+ * reports. A caller that wants to react to it reads `lastError` or calls `refresh()` itself and
23
+ * awaits/catches that promise directly.
24
+ *
25
+ * @param requiredArgs - the session port created by `createVibecodingSession`.
26
+ * @returns the current snapshot plus the session's actions, bound to `session`.
27
+ * @complexity O(1) beyond whatever `session`'s own methods cost — see `./session.js`.
28
+ */
29
+ export declare function useVibecodingSession({ session }: UseVibecodingSessionArgs): UseVibecodingSessionResult;
30
+ //# sourceMappingURL=use-vibecoding-session.d.ts.map