spexcode 0.7.0-next.3 → 0.7.0-next.4

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-application",
3
- "version": "0.7.0-next.3",
3
+ "version": "0.7.0-next.4",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "Adopter-owned composition of session protocol and topology transactions.",
@@ -16,10 +16,10 @@
16
16
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
17
  },
18
18
  "dependencies": {
19
- "@spexcode/session-events": "0.7.0-next.3",
20
- "@spexcode/session-protocol": "0.7.0-next.3",
21
- "@spexcode/session-runtime": "0.7.0-next.3",
22
- "@spexcode/session-topology": "0.7.0-next.3"
19
+ "@spexcode/session-events": "0.7.0-next.4",
20
+ "@spexcode/session-protocol": "0.7.0-next.4",
21
+ "@spexcode/session-runtime": "0.7.0-next.4",
22
+ "@spexcode/session-topology": "0.7.0-next.4"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^20.16.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-events",
3
- "version": "0.7.0-next.3",
3
+ "version": "0.7.0-next.4",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "Append-only session facts and deterministic ordered replay inputs.",
@@ -15,7 +15,7 @@
15
15
  "prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
16
16
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
17
  },
18
- "dependencies": { "@spexcode/session-protocol": "0.7.0-next.3" },
18
+ "dependencies": { "@spexcode/session-protocol": "0.7.0-next.4" },
19
19
  "devDependencies": {
20
20
  "@types/node": "^20.16.0",
21
21
  "tsx": "^4.19.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-protocol",
3
- "version": "0.7.0-next.3",
3
+ "version": "0.7.0-next.4",
4
4
  "type": "module",
5
5
  "description": "A durable SQLite session message protocol.",
6
6
  "files": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-runtime",
3
- "version": "0.7.0-next.3",
3
+ "version": "0.7.0-next.4",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "Adopter-owned bindings between protocol addresses and native runtime identities.",
@@ -15,7 +15,7 @@
15
15
  "prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
16
16
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
17
  },
18
- "dependencies": { "@spexcode/session-protocol": "0.7.0-next.3" },
18
+ "dependencies": { "@spexcode/session-protocol": "0.7.0-next.4" },
19
19
  "devDependencies": {
20
20
  "@types/node": "^20.16.0",
21
21
  "tsx": "^4.19.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-selflaunch",
3
- "version": "0.7.0-next.3",
3
+ "version": "0.7.0-next.4",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "A fail-closed self-launch adopter for the SpexCode session protocol.",
@@ -24,8 +24,8 @@
24
24
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
25
25
  },
26
26
  "dependencies": {
27
- "@spexcode/session-protocol": "0.7.0-next.3",
28
- "@spexcode/session-runtime": "0.7.0-next.3"
27
+ "@spexcode/session-protocol": "0.7.0-next.4",
28
+ "@spexcode/session-runtime": "0.7.0-next.4"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^20.16.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-topology",
3
- "version": "0.7.0-next.3",
3
+ "version": "0.7.0-next.4",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "A neutral SQLite relation model for session addresses.",
@@ -15,7 +15,7 @@
15
15
  "prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
16
16
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
17
  },
18
- "dependencies": { "@spexcode/session-protocol": "0.7.0-next.3" },
18
+ "dependencies": { "@spexcode/session-protocol": "0.7.0-next.4" },
19
19
  "devDependencies": {
20
20
  "@types/node": "^20.16.0",
21
21
  "tsx": "^4.19.2",
@@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs';
3
3
  import { loadSpecs, requireGitWorkspace, headSha } from '@spexcode/spec-core';
4
4
  import { resolveLayout } from '@spexcode/spec-core';
5
5
  import { listSessions } from './sessions.js';
6
- import { driftIndex, historyIndex, repoRoot } from '@spexcode/spec-core';
6
+ import { driftIndex, historyIndex, pruneHistoryCaches, repoRoot } from '@spexcode/spec-core';
7
7
  import { residentForgeRevision, residentForgeState } from '@spexcode/spec-forge/resident';
8
8
  import { resolveForgeHost } from '@spexcode/spec-forge/drivers';
9
9
  import { boardThreads } from './issues.js';
@@ -56,6 +56,9 @@ export async function boardSnapshot() {
56
56
  const root = repoRoot();
57
57
  requireGitWorkspace(root);
58
58
  const [specs, sessions] = await Promise.all([loadSpecs(), listSessions()]);
59
+ // Session worktrees are the live-root census for immutable history caches. Reconcile before the snapshot
60
+ // returns so closing a session releases its full index immediately rather than waiting for an LRU slot.
61
+ pruneHistoryCaches([root, ...sessions.map((session) => session.path)]);
59
62
  const layout = await resolveLayout({ activeSessionIds: sessions.map((session) => session.id) });
60
63
  const nodeIds = [...new Set([
61
64
  ...specs.map((node) => node.id),
@@ -2784,7 +2784,9 @@ const socketListenerLiveness = (_rec, tmuxAlive, _runtimeDir, _pane, socketLive)
2784
2784
  const socketListenerOrPidAliveLiveness = (_rec, tmuxAlive, _runtimeDir, pane, socketLive) => (tmuxAlive && (!!socketLive || pane?.pidAlive === true) ? 'online' : 'offline');
2785
2785
  const panePidLiveness = (_rec, tmuxAlive, _runtimeDir, pane) => (tmuxAlive && pane?.pidAlive === true ? 'online' : 'offline');
2786
2786
  const recordOnline = (rec) => rec.stopped ? 'offline' : 'online';
2787
- const sessionHomeLiveness = (_rec, tmuxAlive) => tmuxAlive ? 'online' : 'offline';
2787
+ // Leaf-backed headless sessions are the controller process, not the tmux pane that hosts it. The pane can
2788
+ // survive a SIGKILL as a bare shell, so tmux presence without the launch-registered PID is never online.
2789
+ const sessionHomeLiveness = (_rec, tmuxAlive, _runtimeDir, pane) => (tmuxAlive && pane?.pidAlive === true ? 'online' : 'offline');
2788
2790
  // @@@ unlinkSocks - remove ONLY the transport this teardown PROVED dead. `cleanupRuntime` unlinks *their*
2789
2791
  // socket, and the honest test of "theirs" is that the agent it just killed is GONE. It used to unlink on
2790
2792
  // faith, which is unsound because a socket path is derived from the session id ALONE: it is the one
@@ -2889,6 +2891,7 @@ export const claudeHeadlessHarness = {
2889
2891
  paneTitleIsSelfSummary: false,
2890
2892
  launchCmd: (id, runtimeDir, cmd) => claudeHeadlessLaunchCommand(id, runtimeDir ?? runtimeRoot(), claudeBaseCmd(cmd)),
2891
2893
  launchEnv: noLaunchEnv,
2894
+ // The controller's registered PID is the liveness witness; the tmux home is only the address boundary.
2892
2895
  liveness: sessionHomeLiveness,
2893
2896
  deliveryTransport: unprovenDeliveryTransport,
2894
2897
  deliver: deliverViaClaudeHeadless,
@@ -3400,6 +3403,7 @@ export const piHeadlessHarness = {
3400
3403
  runtimeOwnership: 'leaf',
3401
3404
  paneTitleIsSelfSummary: false,
3402
3405
  launchCmd: (id, runtimeDir, cmd) => piHeadlessLaunchCommand(id, runtimeDir ?? runtimeRoot(), piBaseCmd(cmd)),
3406
+ // The controller's registered PID is the liveness witness; the tmux home is only the address boundary.
3403
3407
  liveness: sessionHomeLiveness,
3404
3408
  deliver: deliverViaPiHeadless,
3405
3409
  // the controller aborts its own turn child natively and confirms only once that child is gone
@@ -3513,6 +3517,7 @@ export const opencodeHeadlessHarness = {
3513
3517
  runtimeOwnership: 'leaf',
3514
3518
  deliveryTransport: unprovenDeliveryTransport,
3515
3519
  launchCmd: (_id, _runtimeDir, cmd) => opencodeHeadlessLaunchCommand(opencodeBaseCmd(cmd)),
3520
+ // The controller's registered PID is the liveness witness; the tmux home is only the address boundary.
3516
3521
  liveness: sessionHomeLiveness,
3517
3522
  coldRuntime: async (rec) => {
3518
3523
  const result = await opencodeHeadlessColdRuntime(rec);
@@ -216,6 +216,29 @@ const unresolvedHarnessOwners = (recs, budgets, have) => {
216
216
  }
217
217
  return out;
218
218
  };
219
+ // A leaf-backed headless controller is the session's runtime. Keep its durable row visible when the controller
220
+ // disappears before the next resources sweep can attribute a process, and name the status/liveness contradiction
221
+ // explicitly so a supervisor cannot mistake the missing process for a cleanly idle session.
222
+ const missingHeadlessControllerOwners = (recs, procs, budgets, have) => {
223
+ const out = [];
224
+ for (const rec of recs) {
225
+ const harness = harnessByIdOrNull(rec.harness || defaultHarness.id);
226
+ if (!rec.governed || rec.stopped || rec.archived || rec.status !== 'active' && rec.status !== 'idle'
227
+ || harness?.runtimeOwnership !== 'leaf' || have(rec.session_id))
228
+ continue;
229
+ const pid = runtimePid(join(runtimeRoot(), 'sessions', rec.session_id, 'agent.pid'));
230
+ if (pid && procs.has(pid))
231
+ continue;
232
+ out.push({
233
+ kind: 'session', id: rec.session_id, label: `session ${rec.session_id.slice(0, 8)}`, status: rec.status, liveness: 'offline',
234
+ processes: [], rssMiB: 0, pssMiB: null, cpuPercent: 0,
235
+ budget: { rssMiB: budgets.sessionRssMiB, idleCpuPercent: budgets.idleCpuPercent },
236
+ findings: [`headless-liveness-contradiction:${rec.status}/offline`],
237
+ reclaim: { eligible: false, reason: 'headless controller is absent; lifecycle repair must be explicit' },
238
+ });
239
+ }
240
+ return out;
241
+ };
219
242
  const sharedDescriptors = (recs, retainRegistry = false) => {
220
243
  const out = new Map();
221
244
  for (const harness of HARNESSES)
@@ -722,6 +745,7 @@ export async function collectResourceReport(opts = {}) {
722
745
  budget: { rssMiB: budgets.sessionRssMiB, idleCpuPercent: budgets.idleCpuPercent },
723
746
  findings: [`session-record-corrupt:${entry.error}`], reclaim: { eligible: false, reason: 'session record is corrupt; ownership and lifecycle are unknown' },
724
747
  });
748
+ owners.push(...missingHeadlessControllerOwners(inv.recs, inv.procs, budgets, (sessionId) => owners.some((owner) => owner.kind === 'session' && owner.id === sessionId)));
725
749
  owners.push(...unresolvedHarnessOwners(inv.recs, budgets, (sessionId) => owners.some((owner) => owner.kind === 'session' && owner.id === sessionId)));
726
750
  // A referenced shared runtime with no readable process is still operationally important: keep its live or
727
751
  // unknown refcount visible instead of silently omitting it from a process-derived report.
@@ -2538,12 +2538,19 @@ async function establishBackendConnection(target) {
2538
2538
  socket.destroy();
2539
2539
  fn();
2540
2540
  };
2541
- const timer = setTimeout(() => finish(() => {
2542
- const error = new Error(`backend connection was not accepted at ${target.url} within 1500ms`);
2543
- error.name = 'BackendError';
2544
- Object.assign(error, { code: 'backend_availability_indeterminate' });
2545
- reject(error);
2546
- }), 1500);
2541
+ const timer = setTimeout(() => {
2542
+ // The event loop may have been blocked past the wall after the kernel completed the handshake. In that
2543
+ // case Node has not emitted `connect` yet, but `connecting` is already false; acceptance still proves
2544
+ // presence and must not be relabelled as an unavailable backend.
2545
+ if (!socket.connecting || socket.readyState === 'open')
2546
+ return finish(() => resolve(false));
2547
+ finish(() => {
2548
+ const error = new Error(`backend connection was not accepted at ${target.url} within 1500ms`);
2549
+ error.name = 'BackendError';
2550
+ Object.assign(error, { code: 'backend_availability_indeterminate' });
2551
+ reject(error);
2552
+ });
2553
+ }, 1500);
2547
2554
  timer.unref?.();
2548
2555
  socket.once('connect', () => finish(() => resolve(false)));
2549
2556
  socket.once('error', (error) => finish(() => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-cli",
3
- "version": "0.7.0-next.3",
3
+ "version": "0.7.0-next.4",
4
4
  "type": "module",
5
5
  "description": "SpexCode CLI + server. The root spexcode package delegates to this compiled package; dashboard assets live in @spexcode/spec-dashboard.",
6
6
  "bin": {
@@ -35,12 +35,12 @@
35
35
  "test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
36
36
  },
37
37
  "dependencies": {
38
- "@spexcode/session-application": "0.7.0-next.3",
39
- "@spexcode/session-selflaunch": "0.7.0-next.3",
40
- "@spexcode/spec-core": "0.7.0-next.3",
41
- "@spexcode/spec-eval": "0.7.0-next.3",
42
- "@spexcode/spec-forge": "0.7.0-next.3",
43
- "@spexcode/transcript": "0.7.0-next.3",
38
+ "@spexcode/session-application": "0.7.0-next.4",
39
+ "@spexcode/session-selflaunch": "0.7.0-next.4",
40
+ "@spexcode/spec-core": "0.7.0-next.4",
41
+ "@spexcode/spec-eval": "0.7.0-next.4",
42
+ "@spexcode/spec-forge": "0.7.0-next.4",
43
+ "@spexcode/transcript": "0.7.0-next.4",
44
44
  "smol-toml": "^1.8.0"
45
45
  },
46
46
  "devDependencies": {
@@ -122,6 +122,7 @@ export declare function historyCacheStats(): {
122
122
  historyRoots: number;
123
123
  driftRoots: number;
124
124
  };
125
+ export declare function pruneHistoryCaches(activeRoots: Iterable<string>): void;
125
126
  export declare function resetHistoryCachesForTests(): void;
126
127
  export declare function historyEventCachePathForTests(root: string): string;
127
128
  export declare function ancestorsOf(idx: Reachability, sha: string): Uint8Array | undefined;
@@ -2270,6 +2270,31 @@ export function sourceIndexes(root, tip = 'HEAD') {
2270
2270
  export function historyCacheStats() {
2271
2271
  return { historyHeads: indexCache.size, driftHeads: driftIdxCache.size, historyRoots: indexRoots.size, driftRoots: driftRoots.size };
2272
2272
  }
2273
+ // A backend snapshot is the authority for which checkout roots are live. Release cache ownership for roots
2274
+ // that disappeared from that snapshot so closed session worktrees do not keep immutable history warm until
2275
+ // the generic slot bound happens to evict them. Shared entries survive while another live root still names
2276
+ // the same immutable key; an in-flight promise remains usable by its current caller even after its cache key
2277
+ // is released.
2278
+ export function pruneHistoryCaches(activeRoots) {
2279
+ const active = new Set([...activeRoots].map(rootKey));
2280
+ for (const [root] of indexRoots)
2281
+ if (!active.has(root))
2282
+ indexRoots.delete(root);
2283
+ for (const [root] of driftRoots)
2284
+ if (!active.has(root))
2285
+ driftRoots.delete(root);
2286
+ const referencedHistory = new Set(indexRoots.values());
2287
+ const referencedDrift = new Set(driftRoots.values());
2288
+ for (const key of indexCache.keys())
2289
+ if (!referencedHistory.has(key))
2290
+ indexCache.delete(key);
2291
+ for (const key of driftIdxCache.keys())
2292
+ if (!referencedDrift.has(key))
2293
+ driftIdxCache.delete(key);
2294
+ for (const root of eventPathMemo.keys())
2295
+ if (!active.has(root))
2296
+ eventPathMemo.delete(root);
2297
+ }
2273
2298
  // Tests deliberately clear process-local promises and path identity to exercise both cold and read-back paths.
2274
2299
  // Production callers retain the bounded index caches; this is not a correctness escape hatch.
2275
2300
  export function resetHistoryCachesForTests() {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-core",
3
- "version": "0.7.0-next.3",
3
+ "version": "0.7.0-next.4",
4
4
  "type": "module",
5
5
  "description": "SpexCode's dependency-minimal spec graph core.",
6
6
  "files": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-eval",
3
- "version": "0.7.0-next.3",
3
+ "version": "0.7.0-next.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
27
27
  },
28
28
  "dependencies": {
29
- "@spexcode/spec-core": "0.7.0-next.3"
29
+ "@spexcode/spec-core": "0.7.0-next.4"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^20.16.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-forge",
3
- "version": "0.7.0-next.3",
3
+ "version": "0.7.0-next.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "test": "tsx --test src/*.test.ts"
23
23
  },
24
24
  "dependencies": {
25
- "@spexcode/spec-core": "0.7.0-next.3"
25
+ "@spexcode/spec-core": "0.7.0-next.4"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^20.16.0",
@@ -104,6 +104,9 @@ export function claudeEvent(value) {
104
104
  }
105
105
  return null;
106
106
  }
107
+ const blockText = (content) => typeof content === 'string'
108
+ ? string(content)
109
+ : items(content).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
107
110
  export function codexEvent(value) {
108
111
  const entry = object(value);
109
112
  const payload = object(entry?.payload);
@@ -113,16 +116,25 @@ export function codexEvent(value) {
113
116
  if (eventAt === null)
114
117
  return { at: null, turn: null };
115
118
  const type = string(payload.type);
116
- if ((entry.type === 'event_msg' && type === 'user_message')
117
- || (entry.type === 'response_item' && (type === 'message' || type === 'input_message') && payload.role === 'user')) {
119
+ // THE PERSON'S MESSAGE is the `user_message` event. Codex also records it as a `response_item` message (the API
120
+ // form, `input_text` blocks) beside harness injections that no person typed (the AGENTS.md instructions), so
121
+ // that form is not a turn — the event is, once.
122
+ if (entry.type === 'event_msg' && type === 'user_message') {
118
123
  const text = typeof payload.message === 'string' ? payload.message : compact(payload.message ?? payload.content ?? '');
119
124
  return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'user', text, tools: [] } } : null;
120
125
  }
121
- // commentary AND the final answer are both what the agent said; only structured reasoning stays private
126
+ if (entry.type === 'response_item' && (type === 'message' || type === 'input_message') && payload.role === 'user')
127
+ return { at: eventAt, turn: null };
128
+ // WHAT THE AGENT SAID is the `agent_message` event — commentary and the final answer alike. Codex 0.146 also
129
+ // records the same prose as a `response_item` message (`output_text` blocks) beside it, so that form is not read:
130
+ // reading both would say every sentence twice. An empty event (a final answer that was a tool call) is a clock,
131
+ // not a turn. Structured reasoning stays private.
122
132
  if (entry.type === 'event_msg' && type === 'agent_message') {
123
133
  const text = string(payload.message ?? payload.text);
124
- return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', text, tools: [] } } : null;
134
+ return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', text, tools: [] } } : { at: eventAt, turn: null };
125
135
  }
136
+ if (entry.type === 'response_item' && type === 'message' && payload.role === 'assistant')
137
+ return { at: eventAt, turn: null };
126
138
  if (entry.type === 'response_item' && (type === 'custom_tool_call' || type === 'function_call')) {
127
139
  const id = string(payload.call_id ?? payload.id) ?? 'tool';
128
140
  return { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', tools: [{ id, name: string(payload.name ?? payload.tool_name) ?? 'tool', input: payload.input === undefined && payload.arguments === undefined ? undefined : compact(payload.input ?? payload.arguments), outputLines: 0, outputBytes: 0 }] } };
@@ -229,9 +241,6 @@ export function codexAppServerStream() {
229
241
  return parsed;
230
242
  };
231
243
  }
232
- const blockText = (content) => typeof content === 'string'
233
- ? string(content)
234
- : items(content).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
235
244
  export function piEvent(value) {
236
245
  const entry = object(value);
237
246
  const message = object(entry?.message);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/transcript",
3
- "version": "0.7.0-next.3",
3
+ "version": "0.7.0-next.4",
4
4
  "type": "module",
5
5
  "description": "Normalized agent transcripts: one parser per harness, a bounded interval reader over a native thread file or an in-memory event stream, and the full/delta frame protocol every transport and renderer share.",
6
6
  "files": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spexcode",
3
- "version": "0.7.0-next.3",
3
+ "version": "0.7.0-next.4",
4
4
  "type": "module",
5
5
  "description": "SpexCode — a spec-driven, self-developing dev tool. The `spex` CLI + spec server reads the .spec tree and its git history, and serves the dashboard.",
6
6
  "license": "MIT",
@@ -44,7 +44,7 @@
44
44
  "prepack": "node scripts/sync-init-plugins.mjs --check && node scripts/prepack.mjs"
45
45
  },
46
46
  "dependencies": {
47
- "@spexcode/spec-cli": "0.7.0-next.3"
47
+ "@spexcode/spec-cli": "0.7.0-next.4"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"