chalkbridge 1.0.1 → 1.0.3
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/CHANGELOG.md +47 -0
- package/README.md +16 -0
- package/dist/{chunk-SHJT5BF3.js → chunk-VTFAXWQJ.js} +11 -3
- package/dist/cli.js +233 -21
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# chalkbridge
|
|
2
2
|
|
|
3
|
+
## 1.0.3
|
|
4
|
+
|
|
5
|
+
**An upgrade now actually arrives, a bridge can be stopped, and it ends when you are
|
|
6
|
+
done with it.**
|
|
7
|
+
|
|
8
|
+
(1.0.2 was built and verified but never published — everything below shipped as 1.0.3.)
|
|
9
|
+
|
|
10
|
+
The bridge is spawned detached and outlives the session that started it — deliberately,
|
|
11
|
+
because writes have to succeed with no browser connected. But `ensureBridge` reused
|
|
12
|
+
whatever was already listening without ever checking its version, so `npx -y chalkbridge
|
|
13
|
+
mcp` would fetch a new package, find the **old** bridge still answering, and reuse it.
|
|
14
|
+
Every fix would ship and none would arrive, silently. If 1.0.1 seemed not to change
|
|
15
|
+
anything for you, that is why.
|
|
16
|
+
|
|
17
|
+
- **A bridge from another build is replaced.** It is asked to stop first, so its pending
|
|
18
|
+
`.excalidraw` write still reaches the disk.
|
|
19
|
+
- **`chalkbridge stop`** ends the bridge for the current repo, flushing anything unsaved.
|
|
20
|
+
Until now the only options were a reboot or hunting the pid.
|
|
21
|
+
- **`POST /api/shutdown`** is what both use — a request rather than a signal, because on
|
|
22
|
+
Windows a signal from another process terminates outright and the flush never happens.
|
|
23
|
+
- **A pid fallback for bridges built before this release.** 1.0.0 and 1.0.1 have no
|
|
24
|
+
shutdown endpoint, so the polite path is unavailable for exactly the bridges an upgrade
|
|
25
|
+
has to replace. Without it, upgrading from either would leave the new build unable to
|
|
26
|
+
start at all. Found by running the published 1.0.1 against a local build.
|
|
27
|
+
|
|
28
|
+
**And it ends when you are done with it.** Until now a bridge ran until the machine
|
|
29
|
+
restarted — one per repo, forever — which is not what anybody expects after closing a chat
|
|
30
|
+
and a tab. It now exits when nothing is watching *and* nothing is asking, for 90 seconds:
|
|
31
|
+
|
|
32
|
+
| | |
|
|
33
|
+
|---|---|
|
|
34
|
+
| chat closed, tab closed | exits |
|
|
35
|
+
| tab refreshed | reconnects in about a second, so it stays |
|
|
36
|
+
| chat closed, tab still open | stays — somebody is looking at the canvas |
|
|
37
|
+
| Claude drawing with no tab open | every tool call counts, so it stays |
|
|
38
|
+
| two chats on one repo | activity keeps it alive |
|
|
39
|
+
|
|
40
|
+
One rule, no session counting. `CHALK_IDLE_TIMEOUT` tunes it and `0` restores the old
|
|
41
|
+
behaviour.
|
|
42
|
+
|
|
43
|
+
**A tool call now survives its own bridge.** That is what makes the above invisible: if a
|
|
44
|
+
bridge has given up, the next call re-runs the bootstrap — spawning one and reopening the
|
|
45
|
+
canvas — instead of failing. Previously a bridge lost mid-session meant every call failed
|
|
46
|
+
until Claude Code was restarted.
|
|
47
|
+
|
|
48
|
+
After this release, updating really is just restarting Claude Code.
|
|
49
|
+
|
|
3
50
|
## 1.0.1
|
|
4
51
|
|
|
5
52
|
**Arrows are drawn where they belong, the first time.**
|
package/README.md
CHANGED
|
@@ -53,10 +53,26 @@ reach loopback. Do not work around it by binding a wider address.
|
|
|
53
53
|
```bash
|
|
54
54
|
chalkbridge health # what is running for this repository
|
|
55
55
|
chalkbridge serve # start the bridge by hand
|
|
56
|
+
chalkbridge stop # end it, flushing anything unsaved
|
|
56
57
|
chalkbridge icons list # what is in the machine-wide icon catalog
|
|
57
58
|
chalkbridge icons install devicon
|
|
58
59
|
```
|
|
59
60
|
|
|
61
|
+
**The bridge is spawned detached and outlives your session** — deliberately, because
|
|
62
|
+
writes have to succeed with no browser connected. It ends when nothing is watching *and*
|
|
63
|
+
nothing is asking:
|
|
64
|
+
|
|
65
|
+
| | |
|
|
66
|
+
|---|---|
|
|
67
|
+
| chat closed, tab closed | exits after 90 seconds |
|
|
68
|
+
| tab refreshed | reconnects in about a second, so it stays |
|
|
69
|
+
| chat closed, tab still open | stays — you are still looking at the canvas |
|
|
70
|
+
| Claude drawing with no tab | every tool call counts, so it stays |
|
|
71
|
+
|
|
72
|
+
If it has already given up, the next tool call brings it back and reopens the canvas, so
|
|
73
|
+
you never have to think about it. `chalkbridge stop` ends one now; `CHALK_IDLE_TIMEOUT`
|
|
74
|
+
tunes the ninety seconds, and `0` keeps it up forever.
|
|
75
|
+
|
|
60
76
|
`CHALK_NO_AUTOSTART=1` and `CHALK_NO_OPEN=1` turn off the two automatic behaviours;
|
|
61
77
|
`CHALK_PORT` pins the port; `CHALK_DATA_DIR` moves the icon catalog.
|
|
62
78
|
|
|
@@ -1248,6 +1248,14 @@ var bridgeUnreachable = (port) => port === null ? err("BRIDGE_UNREACHABLE", "no
|
|
|
1248
1248
|
hint: "the bridge may have exited \u2014 `chalkbridge health` reports what is running",
|
|
1249
1249
|
detail: { port }
|
|
1250
1250
|
});
|
|
1251
|
+
var bridgeVersionMismatch = (running, ours, port) => err(
|
|
1252
|
+
"BRIDGE_VERSION_MISMATCH",
|
|
1253
|
+
`a Chalkbridge ${running} bridge is running on 127.0.0.1:${port} and this is ${ours}`,
|
|
1254
|
+
{
|
|
1255
|
+
hint: "it did not stop when asked \u2014 run `chalkbridge stop` in this repo, or end that process, and try again",
|
|
1256
|
+
detail: { running, ours, port }
|
|
1257
|
+
}
|
|
1258
|
+
);
|
|
1251
1259
|
var portUnavailable = (from, tried) => err("PORT_UNAVAILABLE", `no free port between ${from} and ${from + tried - 1}`, {
|
|
1252
1260
|
hint: "something is occupying the whole range \u2014 set CHALK_PORT to pick a different one",
|
|
1253
1261
|
detail: { from, tried }
|
|
@@ -1495,6 +1503,6 @@ var stable = (element) => {
|
|
|
1495
1503
|
return rest;
|
|
1496
1504
|
};
|
|
1497
1505
|
|
|
1498
|
-
export { AnnotateInput, AnnotateOutput, ApiError, ArrangeOutput, ArrangeToolInput, BINDING_FIELDS, ClientMessage, ConnectOutput, ConnectToolInput, CreateElementsInput, CreateElementsOutput, DebugElementRequest, DeleteElementsInput, DeleteElementsOutput, DescribeSceneInput, DescribeSceneOutput, DetailLevel, EXCALIDRAW_FILE_TYPE, EXCALIDRAW_FILE_VERSION, ElementId, EmbedInput, EmbedOutput, ExcalidrawFile, ExportImageInput, ExportImageOutput, ExportSceneInput, ExportSceneOutput, FLAG_GLYPH, FileId, FocusInput, FocusOutput, FrameId, FrameInput, FrameOutput, GUIDE_TOPICS, GetElementsInput, GetElementsOutput, GetSelectionInput, GetSelectionOutput, GroupId, GroupInput, GroupOutput, GuideTopic, HOUSE, HealthResponse, IconId, IconItem, ImportSceneInput, ImportSceneOutput, Index, InstallPackInput, InstallPackOutput, KIND_ABBREVIATION, ListPacksInput, ListPacksOutput, ListScenesInput, ListScenesOutput, PROTOCOL_VERSION, PackKind, PackName, PackRecipe, PlaceIconInput, PlaceIconOutput, ROUNDNESS, RawElementsInput, RawElementsOutput, ReadMeInput, ReadMeOutput, RemovePackInput, RemovePackOutput, RestoreSnapshotInput, RestoreSnapshotOutput, SERVER_MANAGED_FIELDS, STALE_SELECTION_MS, Scene, SceneElement, SceneId, SceneSettingsInput, SceneSettingsOutput, ScreenshotInput, ScreenshotOutput, ScreenshotResponse, SearchIconsInput, SearchIconsOutput, SnapshotId, SnapshotInput, SnapshotOutput, UngroupInput, UngroupOutput, UpdateElementsInput, UpdateElementsOutput, assertNever, bridgeSpawnFailed, bridgeUnreachable, catalogUnavailable, createNodeSkeletonExpander, deepEqual, describeThrown, diskConflict, emptySelection, expansionFailed, exportFailed, exportTimedOut, fail, frameIdOf, iconIdOf, idCollision, isFail, isOk, noBoundLabel, noBrowser, noScenes, notAPack, notARepo, ok, packInstallFailed, partition, pathOutsideRepo, point, portUnavailable, refusedFieldMessage, sceneFileMissing, staleVersion, storeUnavailable, unknownElement, unknownIcon, unknownScene, unknownSnapshot, unreadableSceneFile };
|
|
1499
|
-
//# sourceMappingURL=chunk-
|
|
1500
|
-
//# sourceMappingURL=chunk-
|
|
1506
|
+
export { AnnotateInput, AnnotateOutput, ApiError, ArrangeOutput, ArrangeToolInput, BINDING_FIELDS, ClientMessage, ConnectOutput, ConnectToolInput, CreateElementsInput, CreateElementsOutput, DebugElementRequest, DeleteElementsInput, DeleteElementsOutput, DescribeSceneInput, DescribeSceneOutput, DetailLevel, EXCALIDRAW_FILE_TYPE, EXCALIDRAW_FILE_VERSION, ElementId, EmbedInput, EmbedOutput, ExcalidrawFile, ExportImageInput, ExportImageOutput, ExportSceneInput, ExportSceneOutput, FLAG_GLYPH, FileId, FocusInput, FocusOutput, FrameId, FrameInput, FrameOutput, GUIDE_TOPICS, GetElementsInput, GetElementsOutput, GetSelectionInput, GetSelectionOutput, GroupId, GroupInput, GroupOutput, GuideTopic, HOUSE, HealthResponse, IconId, IconItem, ImportSceneInput, ImportSceneOutput, Index, InstallPackInput, InstallPackOutput, KIND_ABBREVIATION, ListPacksInput, ListPacksOutput, ListScenesInput, ListScenesOutput, PROTOCOL_VERSION, PackKind, PackName, PackRecipe, PlaceIconInput, PlaceIconOutput, ROUNDNESS, RawElementsInput, RawElementsOutput, ReadMeInput, ReadMeOutput, RemovePackInput, RemovePackOutput, RestoreSnapshotInput, RestoreSnapshotOutput, SERVER_MANAGED_FIELDS, STALE_SELECTION_MS, Scene, SceneElement, SceneId, SceneSettingsInput, SceneSettingsOutput, ScreenshotInput, ScreenshotOutput, ScreenshotResponse, SearchIconsInput, SearchIconsOutput, SnapshotId, SnapshotInput, SnapshotOutput, UngroupInput, UngroupOutput, UpdateElementsInput, UpdateElementsOutput, assertNever, bridgeSpawnFailed, bridgeUnreachable, bridgeVersionMismatch, catalogUnavailable, createNodeSkeletonExpander, deepEqual, describeThrown, diskConflict, emptySelection, expansionFailed, exportFailed, exportTimedOut, fail, frameIdOf, iconIdOf, idCollision, isFail, isOk, noBoundLabel, noBrowser, noScenes, notAPack, notARepo, ok, packInstallFailed, partition, pathOutsideRepo, point, portUnavailable, refusedFieldMessage, sceneFileMissing, staleVersion, storeUnavailable, unknownElement, unknownIcon, unknownScene, unknownSnapshot, unreadableSceneFile };
|
|
1507
|
+
//# sourceMappingURL=chunk-VTFAXWQJ.js.map
|
|
1508
|
+
//# sourceMappingURL=chunk-VTFAXWQJ.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { SceneElement, PackName, PackKind, IconId, SceneId, Scene, SnapshotId, SERVER_MANAGED_FIELDS, BINDING_FIELDS, GUIDE_TOPICS, DetailLevel, createNodeSkeletonExpander, ok, fail, bridgeSpawnFailed, bridgeUnreachable, GuideTopic, storeUnavailable, describeThrown, unreadableSceneFile, pathOutsideRepo, packInstallFailed, sceneFileMissing, notAPack, PackRecipe, portUnavailable, DescribeSceneOutput, DescribeSceneInput, GetSelectionOutput, GetSelectionInput, GetElementsOutput, GetElementsInput, ListScenesOutput, ListScenesInput, SceneSettingsOutput, SceneSettingsInput, SearchIconsOutput, SearchIconsInput, ScreenshotOutput, ScreenshotInput, ReadMeOutput, ReadMeInput, PlaceIconOutput, PlaceIconInput, SnapshotOutput, SnapshotInput, RestoreSnapshotOutput, RestoreSnapshotInput, ExportSceneOutput, ExportSceneInput, ImportSceneOutput, ImportSceneInput, ExportImageOutput, ExportImageInput, InstallPackOutput, InstallPackInput, notARepo, ScreenshotResponse, RemovePackOutput, ListPacksOutput, RawElementsOutput, FocusOutput, EmbedOutput, AnnotateOutput, FrameOutput, UngroupOutput, GroupOutput, ArrangeOutput, DeleteElementsOutput, UpdateElementsOutput, ConnectOutput, CreateElementsOutput, HealthResponse, iconIdOf, unknownScene, unknownSnapshot, exportFailed, unknownIcon, HOUSE, FileId, ElementId, diskConflict, refusedFieldMessage, idCollision, expansionFailed, emptySelection, unknownElement, frameIdOf, GroupId, staleVersion, noBoundLabel, deepEqual, exportTimedOut, noBrowser, DebugElementRequest, CreateElementsInput, ConnectToolInput, UpdateElementsInput, DeleteElementsInput, ArrangeToolInput, GroupInput, UngroupInput, FrameInput, AnnotateInput, EmbedInput, FocusInput, RawElementsInput, ListPacksInput, RemovePackInput, Index, ROUNDNESS, STALE_SELECTION_MS, noScenes, ClientMessage, assertNever, PROTOCOL_VERSION, ApiError, EXCALIDRAW_FILE_VERSION, EXCALIDRAW_FILE_TYPE, point, FLAG_GLYPH, KIND_ABBREVIATION, catalogUnavailable, FrameId, IconItem, ExcalidrawFile } from './chunk-
|
|
2
|
+
import { SceneElement, PackName, PackKind, IconId, SceneId, Scene, SnapshotId, SERVER_MANAGED_FIELDS, BINDING_FIELDS, GUIDE_TOPICS, DetailLevel, createNodeSkeletonExpander, ok, fail, bridgeSpawnFailed, bridgeVersionMismatch, bridgeUnreachable, GuideTopic, storeUnavailable, describeThrown, unreadableSceneFile, pathOutsideRepo, packInstallFailed, sceneFileMissing, notAPack, PackRecipe, portUnavailable, DescribeSceneOutput, DescribeSceneInput, GetSelectionOutput, GetSelectionInput, GetElementsOutput, GetElementsInput, ListScenesOutput, ListScenesInput, SceneSettingsOutput, SceneSettingsInput, SearchIconsOutput, SearchIconsInput, ScreenshotOutput, ScreenshotInput, ReadMeOutput, ReadMeInput, PlaceIconOutput, PlaceIconInput, SnapshotOutput, SnapshotInput, RestoreSnapshotOutput, RestoreSnapshotInput, ExportSceneOutput, ExportSceneInput, ImportSceneOutput, ImportSceneInput, ExportImageOutput, ExportImageInput, InstallPackOutput, InstallPackInput, notARepo, ScreenshotResponse, RemovePackOutput, ListPacksOutput, RawElementsOutput, FocusOutput, EmbedOutput, AnnotateOutput, FrameOutput, UngroupOutput, GroupOutput, ArrangeOutput, DeleteElementsOutput, UpdateElementsOutput, ConnectOutput, CreateElementsOutput, HealthResponse, iconIdOf, unknownScene, unknownSnapshot, exportFailed, unknownIcon, HOUSE, FileId, ElementId, diskConflict, refusedFieldMessage, idCollision, expansionFailed, emptySelection, unknownElement, frameIdOf, GroupId, staleVersion, noBoundLabel, deepEqual, exportTimedOut, noBrowser, DebugElementRequest, CreateElementsInput, ConnectToolInput, UpdateElementsInput, DeleteElementsInput, ArrangeToolInput, GroupInput, UngroupInput, FrameInput, AnnotateInput, EmbedInput, FocusInput, RawElementsInput, ListPacksInput, RemovePackInput, Index, ROUNDNESS, STALE_SELECTION_MS, noScenes, ClientMessage, assertNever, PROTOCOL_VERSION, ApiError, EXCALIDRAW_FILE_VERSION, EXCALIDRAW_FILE_TYPE, point, FLAG_GLYPH, KIND_ABBREVIATION, catalogUnavailable, FrameId, IconItem, ExcalidrawFile } from './chunk-VTFAXWQJ.js';
|
|
3
3
|
import './chunk-7D4SUZUM.js';
|
|
4
4
|
import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
@@ -1046,6 +1046,7 @@ var KNOWN_CODES = /* @__PURE__ */ new Set([
|
|
|
1046
1046
|
"BRIDGE_SPAWN_FAILED",
|
|
1047
1047
|
"BRIDGE_UNREACHABLE",
|
|
1048
1048
|
"PORT_UNAVAILABLE",
|
|
1049
|
+
"BRIDGE_VERSION_MISMATCH",
|
|
1049
1050
|
"STALE_VERSION",
|
|
1050
1051
|
"ID_COLLISION",
|
|
1051
1052
|
"UNKNOWN_ELEMENT",
|
|
@@ -1099,23 +1100,48 @@ function createBridgeClient() {
|
|
|
1099
1100
|
return null;
|
|
1100
1101
|
}
|
|
1101
1102
|
},
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1103
|
+
async shutdown(port, timeoutMs = DEFAULT_HEALTH_TIMEOUT_MS) {
|
|
1104
|
+
try {
|
|
1105
|
+
await fetch(`http://127.0.0.1:${port}/api/shutdown`, {
|
|
1106
|
+
method: "POST",
|
|
1107
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1108
|
+
});
|
|
1109
|
+
} catch {
|
|
1110
|
+
}
|
|
1111
|
+
const deadline = Date.now() + timeoutMs * 4;
|
|
1112
|
+
while (Date.now() < deadline) {
|
|
1113
|
+
if (await this.health(port, timeoutMs) === null) return true;
|
|
1114
|
+
await new Promise((resolve6) => setTimeout(resolve6, 100));
|
|
1115
|
+
}
|
|
1116
|
+
return false;
|
|
1117
|
+
},
|
|
1118
|
+
connect(port, reconnect) {
|
|
1119
|
+
let current = port;
|
|
1120
|
+
async function attempt(path, init, timeoutMs) {
|
|
1106
1121
|
try {
|
|
1107
|
-
|
|
1122
|
+
return await fetch(`http://127.0.0.1:${current}${path}`, {
|
|
1108
1123
|
...init,
|
|
1109
1124
|
signal: AbortSignal.timeout(timeoutMs)
|
|
1110
1125
|
});
|
|
1111
1126
|
} catch {
|
|
1112
|
-
return
|
|
1127
|
+
return null;
|
|
1113
1128
|
}
|
|
1129
|
+
}
|
|
1130
|
+
async function call(path, schema, init, timeoutMs = DEFAULT_API_TIMEOUT_MS) {
|
|
1131
|
+
let response = await attempt(path, init, timeoutMs);
|
|
1132
|
+
if (response === null && reconnect !== void 0) {
|
|
1133
|
+
const revived = await reconnect();
|
|
1134
|
+
if (revived !== null) {
|
|
1135
|
+
current = revived;
|
|
1136
|
+
response = await attempt(path, init, timeoutMs);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (response === null) return fail(bridgeUnreachable(current));
|
|
1114
1140
|
const body = await response.json().catch(() => null);
|
|
1115
|
-
if (!response.ok) return fail(toChalkError(body,
|
|
1141
|
+
if (!response.ok) return fail(toChalkError(body, current));
|
|
1116
1142
|
const parsed = schema.safeParse(body);
|
|
1117
1143
|
if (!parsed.success) {
|
|
1118
|
-
return fail(bridgeUnreachable(
|
|
1144
|
+
return fail(bridgeUnreachable(current));
|
|
1119
1145
|
}
|
|
1120
1146
|
return ok(parsed.data);
|
|
1121
1147
|
}
|
|
@@ -1726,6 +1752,24 @@ function createBridgeLauncher(options, deps) {
|
|
|
1726
1752
|
await sleep(pollIntervalMs);
|
|
1727
1753
|
}
|
|
1728
1754
|
return fail(bridgeSpawnFailed(`it did not start listening within ${readyTimeoutMs}ms`));
|
|
1755
|
+
},
|
|
1756
|
+
async stop(pid) {
|
|
1757
|
+
try {
|
|
1758
|
+
process.kill(pid, "SIGTERM");
|
|
1759
|
+
} catch {
|
|
1760
|
+
return false;
|
|
1761
|
+
}
|
|
1762
|
+
const deadline = Date.now() + 5e3;
|
|
1763
|
+
while (Date.now() < deadline) {
|
|
1764
|
+
try {
|
|
1765
|
+
process.kill(pid, 0);
|
|
1766
|
+
} catch {
|
|
1767
|
+
return true;
|
|
1768
|
+
}
|
|
1769
|
+
await sleep(pollIntervalMs);
|
|
1770
|
+
}
|
|
1771
|
+
deps.logger.warn("a bridge did not exit after being signalled", { pid });
|
|
1772
|
+
return false;
|
|
1729
1773
|
}
|
|
1730
1774
|
};
|
|
1731
1775
|
}
|
|
@@ -1734,6 +1778,48 @@ function createBridgeLauncher(options, deps) {
|
|
|
1734
1778
|
function createClock() {
|
|
1735
1779
|
return { now: () => Date.now() };
|
|
1736
1780
|
}
|
|
1781
|
+
|
|
1782
|
+
// src/adapters/system/idle-watch.ts
|
|
1783
|
+
var intervalFor = (timeoutMs) => Math.max(1e3, Math.min(1e4, Math.floor(timeoutMs / 3)));
|
|
1784
|
+
function createIdleWatch(options, deps) {
|
|
1785
|
+
let lastActivity = deps.clock.now();
|
|
1786
|
+
let timer = null;
|
|
1787
|
+
let fired = false;
|
|
1788
|
+
return {
|
|
1789
|
+
touch() {
|
|
1790
|
+
lastActivity = deps.clock.now();
|
|
1791
|
+
},
|
|
1792
|
+
start({ isWatched, onIdle }) {
|
|
1793
|
+
const timeoutMs = options.timeoutMs;
|
|
1794
|
+
if (timeoutMs === null) {
|
|
1795
|
+
deps.logger.debug("idle shutdown is disabled", { reason: "CHALK_IDLE_TIMEOUT=0" });
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (timer !== null) return;
|
|
1799
|
+
const every = options.checkIntervalMs ?? intervalFor(timeoutMs);
|
|
1800
|
+
timer = setInterval(() => {
|
|
1801
|
+
if (fired) return;
|
|
1802
|
+
if (isWatched()) {
|
|
1803
|
+
lastActivity = deps.clock.now();
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
if (deps.clock.now() - lastActivity < timeoutMs) return;
|
|
1807
|
+
fired = true;
|
|
1808
|
+
deps.logger.info("idle \u2014 nothing watching and nothing asking", {
|
|
1809
|
+
idleMs: deps.clock.now() - lastActivity
|
|
1810
|
+
});
|
|
1811
|
+
onIdle();
|
|
1812
|
+
}, every);
|
|
1813
|
+
timer.unref?.();
|
|
1814
|
+
deps.logger.debug("watching for idle", { timeoutMs, every });
|
|
1815
|
+
},
|
|
1816
|
+
stop() {
|
|
1817
|
+
if (timer === null) return;
|
|
1818
|
+
clearInterval(timer);
|
|
1819
|
+
timer = null;
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1737
1823
|
var ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1738
1824
|
var shortId = (length) => {
|
|
1739
1825
|
let out = "";
|
|
@@ -2427,7 +2513,20 @@ function ensureBridge(deps) {
|
|
|
2427
2513
|
const repoRoot = await deps.projectDir.findRepoRoot(options.cwd);
|
|
2428
2514
|
if (!repoRoot.ok) return repoRoot;
|
|
2429
2515
|
const existingPort = await deps.projectDir.readPort(repoRoot.value);
|
|
2430
|
-
|
|
2516
|
+
let existing = existingPort === null ? null : await deps.client.health(existingPort);
|
|
2517
|
+
if (existing !== null && existingPort !== null && existing.version !== deps.version) {
|
|
2518
|
+
deps.logger.info("replacing a bridge from another build", {
|
|
2519
|
+
running: existing.version,
|
|
2520
|
+
ours: deps.version,
|
|
2521
|
+
port: existingPort
|
|
2522
|
+
});
|
|
2523
|
+
const stopped = await deps.client.shutdown(existingPort) || await deps.launcher.stop(existing.pid);
|
|
2524
|
+
if (!stopped) {
|
|
2525
|
+
return fail(bridgeVersionMismatch(existing.version, deps.version, existingPort));
|
|
2526
|
+
}
|
|
2527
|
+
await deps.projectDir.clearPort(repoRoot.value);
|
|
2528
|
+
existing = null;
|
|
2529
|
+
}
|
|
2431
2530
|
let port = existing === null ? null : existingPort;
|
|
2432
2531
|
let health = existing;
|
|
2433
2532
|
let spawned = false;
|
|
@@ -4371,6 +4470,7 @@ function printLine(line) {
|
|
|
4371
4470
|
}
|
|
4372
4471
|
var DEFAULT_PORT = 7180;
|
|
4373
4472
|
var PORT_SCAN_RANGE = 32;
|
|
4473
|
+
var DEFAULT_IDLE_SECONDS = 90;
|
|
4374
4474
|
var truthy = (value) => value !== void 0 && value !== "" && value !== "0" && value.toLowerCase() !== "false";
|
|
4375
4475
|
var EnvSchema = z.object({
|
|
4376
4476
|
CHALK_PORT: z.coerce.number().int().min(1).max(65535).optional(),
|
|
@@ -4383,12 +4483,18 @@ var EnvSchema = z.object({
|
|
|
4383
4483
|
* icon pack into the developer real data directory would be a test that changed the
|
|
4384
4484
|
* machine it ran on.
|
|
4385
4485
|
*/
|
|
4386
|
-
CHALK_DATA_DIR: z.string().min(1).optional()
|
|
4486
|
+
CHALK_DATA_DIR: z.string().min(1).optional(),
|
|
4487
|
+
/**
|
|
4488
|
+
* Seconds a bridge may sit with no browser client and no tool call before it exits.
|
|
4489
|
+
* `0` keeps it up forever, which is what every version before v1.0.2 did.
|
|
4490
|
+
*/
|
|
4491
|
+
CHALK_IDLE_TIMEOUT: z.coerce.number().int().min(0).max(86400).optional()
|
|
4387
4492
|
});
|
|
4388
4493
|
function readEnv(source) {
|
|
4389
4494
|
const parsed = EnvSchema.safeParse(source);
|
|
4390
4495
|
const env = parsed.success ? parsed.data : {};
|
|
4391
4496
|
const explicit2 = env.CHALK_PORT ?? null;
|
|
4497
|
+
const idleSeconds = env.CHALK_IDLE_TIMEOUT ?? DEFAULT_IDLE_SECONDS;
|
|
4392
4498
|
return {
|
|
4393
4499
|
listenFrom: explicit2 ?? DEFAULT_PORT,
|
|
4394
4500
|
listenTries: explicit2 === null ? PORT_SCAN_RANGE : 1,
|
|
@@ -4396,12 +4502,13 @@ function readEnv(source) {
|
|
|
4396
4502
|
autoStart: !truthy(env.CHALK_NO_AUTOSTART),
|
|
4397
4503
|
autoOpen: !truthy(env.CHALK_NO_OPEN),
|
|
4398
4504
|
dataDir: env.CHALK_DATA_DIR ?? null,
|
|
4505
|
+
idleTimeoutMs: idleSeconds === 0 ? null : idleSeconds * 1e3,
|
|
4399
4506
|
logLevel: env.CHALK_LOG_LEVEL ?? "info"
|
|
4400
4507
|
};
|
|
4401
4508
|
}
|
|
4402
4509
|
|
|
4403
4510
|
// src/composition/container.ts
|
|
4404
|
-
var PACKAGE_VERSION = "1.0.
|
|
4511
|
+
var PACKAGE_VERSION = "1.0.3";
|
|
4405
4512
|
function readRuntime() {
|
|
4406
4513
|
return {
|
|
4407
4514
|
env: process.env,
|
|
@@ -4465,6 +4572,7 @@ function buildCommandContext(runtime = readRuntime()) {
|
|
|
4465
4572
|
if (!opened.ok) return opened;
|
|
4466
4573
|
const store = opened.value;
|
|
4467
4574
|
const broadcaster = createBroadcaster(idGen, logger);
|
|
4575
|
+
const idleWatch = createIdleWatch({ timeoutMs: config.idleTimeoutMs }, { clock, logger });
|
|
4468
4576
|
const presenceTracker = createPresenceTracker();
|
|
4469
4577
|
const expander = createNodeSkeletonExpander();
|
|
4470
4578
|
const repoWriter = createRepoWriter({ repoRoot }, { store, logger });
|
|
@@ -4472,6 +4580,7 @@ function buildCommandContext(runtime = readRuntime()) {
|
|
|
4472
4580
|
const diskWatcher = createDiskWatcher({ logger });
|
|
4473
4581
|
const commit = commitScene({ store, repoWriter });
|
|
4474
4582
|
const repoFiles = createRepoFiles({ repoRoot });
|
|
4583
|
+
let onShutdown = null;
|
|
4475
4584
|
const exportBroker = createExportBroker({}, { broadcaster, idGen, logger });
|
|
4476
4585
|
const globalPaths = globalPathsOf(runtime, config);
|
|
4477
4586
|
let catalog = null;
|
|
@@ -4635,6 +4744,10 @@ function buildCommandContext(runtime = readRuntime()) {
|
|
|
4635
4744
|
// logic in `app/`, and a lifecycle command has no more business holding a store
|
|
4636
4745
|
// than a route does.
|
|
4637
4746
|
sceneFilePaths: () => store.list().map((scene) => scene.repoPath).filter((path) => path !== null).map((path) => resolve(repoRoot, path)),
|
|
4747
|
+
idleWatch,
|
|
4748
|
+
onShutdownRequest: (handler) => {
|
|
4749
|
+
onShutdown = handler;
|
|
4750
|
+
},
|
|
4638
4751
|
close: () => {
|
|
4639
4752
|
repoWriter.cancel();
|
|
4640
4753
|
exportBroker.abandon();
|
|
@@ -4675,6 +4788,7 @@ function buildCommandContext(runtime = readRuntime()) {
|
|
|
4675
4788
|
importScene: usecases.importScene,
|
|
4676
4789
|
broadcaster,
|
|
4677
4790
|
identity,
|
|
4791
|
+
requestShutdown: () => onShutdown?.(),
|
|
4678
4792
|
logger
|
|
4679
4793
|
},
|
|
4680
4794
|
ws: {
|
|
@@ -4698,24 +4812,42 @@ function buildCommandContext(runtime = readRuntime()) {
|
|
|
4698
4812
|
{ cliEntry: runtime.entryPath, execPath: runtime.execPath, env: runtime.env },
|
|
4699
4813
|
{ projectDir, client, logger }
|
|
4700
4814
|
);
|
|
4815
|
+
const bootstrap = ensureBridge({
|
|
4816
|
+
// Compared against whatever is already listening: an upgrade that reused an
|
|
4817
|
+
// older detached bridge would ship every fix and deliver none of them.
|
|
4818
|
+
version: runtime.version,
|
|
4819
|
+
projectDir,
|
|
4820
|
+
client,
|
|
4821
|
+
launcher,
|
|
4822
|
+
opener: createBrowserOpener(logger),
|
|
4823
|
+
logger
|
|
4824
|
+
});
|
|
4701
4825
|
return {
|
|
4702
4826
|
logger,
|
|
4703
4827
|
projectDir,
|
|
4704
4828
|
client,
|
|
4829
|
+
launcher,
|
|
4705
4830
|
// A7 — the tool handlers never see a port, a URL, or a `fetch`. They see the
|
|
4706
4831
|
// bridge's read surface and a shelf of markdown, and nothing else exists to
|
|
4707
4832
|
// tempt them into caching a scene (invariant 1).
|
|
4833
|
+
//
|
|
4834
|
+
// The `reconnect` argument is what makes a session survive its own bridge. One
|
|
4835
|
+
// now exits when nothing is watching and nothing is asking, so "the port this
|
|
4836
|
+
// session started with" stops being true the moment the user closes the tab and
|
|
4837
|
+
// walks away. Re-running the bootstrap puts a bridge *and a tab* back, which is
|
|
4838
|
+
// what the session's first call did, so a tool call after a break just works.
|
|
4708
4839
|
createToolDeps: (port) => ({
|
|
4709
|
-
bridge: client.connect(port)
|
|
4840
|
+
bridge: client.connect(port, async () => {
|
|
4841
|
+
const revived = await bootstrap({
|
|
4842
|
+
cwd: runtime.cwd,
|
|
4843
|
+
autoStart: config.autoStart,
|
|
4844
|
+
autoOpen: config.autoOpen
|
|
4845
|
+
});
|
|
4846
|
+
return revived.ok ? revived.value.port : null;
|
|
4847
|
+
}),
|
|
4710
4848
|
guides: createGuideLibrary({ dir: resolveGuidesDir() }, { logger })
|
|
4711
4849
|
}),
|
|
4712
|
-
ensureBridge:
|
|
4713
|
-
projectDir,
|
|
4714
|
-
client,
|
|
4715
|
-
launcher,
|
|
4716
|
-
opener: createBrowserOpener(logger),
|
|
4717
|
-
logger
|
|
4718
|
-
})
|
|
4850
|
+
ensureBridge: bootstrap
|
|
4719
4851
|
};
|
|
4720
4852
|
}
|
|
4721
4853
|
};
|
|
@@ -5702,6 +5834,8 @@ var STATUS = {
|
|
|
5702
5834
|
BRIDGE_SPAWN_FAILED: 500,
|
|
5703
5835
|
BRIDGE_UNREACHABLE: 503,
|
|
5704
5836
|
PORT_UNAVAILABLE: 500,
|
|
5837
|
+
// 409: something is there, it is simply not the build this process belongs to.
|
|
5838
|
+
BRIDGE_VERSION_MISMATCH: 409,
|
|
5705
5839
|
// 409 for both: a stale version and a file that changed underneath us are the same
|
|
5706
5840
|
// shape of problem — the caller's view of the world is out of date, and re-reading is
|
|
5707
5841
|
// the way forward.
|
|
@@ -5960,6 +6094,20 @@ var sessionRoutes = [
|
|
|
5960
6094
|
})
|
|
5961
6095
|
];
|
|
5962
6096
|
|
|
6097
|
+
// src/interfaces/http/routes/shutdown.ts
|
|
6098
|
+
var shutdownRoute = {
|
|
6099
|
+
name: "shutdown",
|
|
6100
|
+
register(app, deps) {
|
|
6101
|
+
app.post("/api/shutdown", async (_request, reply) => {
|
|
6102
|
+
const identity = deps.identity();
|
|
6103
|
+
deps.logger.info("shutdown requested over the API", { pid: identity.pid });
|
|
6104
|
+
await reply.send({ stopping: true, pid: identity.pid, version: identity.version });
|
|
6105
|
+
setTimeout(() => deps.requestShutdown(), 0);
|
|
6106
|
+
return reply;
|
|
6107
|
+
});
|
|
6108
|
+
}
|
|
6109
|
+
};
|
|
6110
|
+
|
|
5963
6111
|
// src/interfaces/http/routes/write.ts
|
|
5964
6112
|
var writeRoutes = [
|
|
5965
6113
|
postRoute({
|
|
@@ -6045,6 +6193,7 @@ var ROUTES = [
|
|
|
6045
6193
|
elementsRoute,
|
|
6046
6194
|
settingsRoute,
|
|
6047
6195
|
debugElementRoute,
|
|
6196
|
+
shutdownRoute,
|
|
6048
6197
|
...writeRoutes,
|
|
6049
6198
|
...sessionRoutes
|
|
6050
6199
|
];
|
|
@@ -6058,6 +6207,9 @@ async function createHttpServer(deps) {
|
|
|
6058
6207
|
bodyLimit: 32 * 1024 * 1024
|
|
6059
6208
|
});
|
|
6060
6209
|
await app.register(websocket);
|
|
6210
|
+
app.addHook("onRequest", async () => {
|
|
6211
|
+
deps.touch();
|
|
6212
|
+
});
|
|
6061
6213
|
for (const route of ROUTES) {
|
|
6062
6214
|
route.register(app, deps.routes);
|
|
6063
6215
|
deps.logger.debug("registered route", { route: route.name });
|
|
@@ -6067,6 +6219,7 @@ async function createHttpServer(deps) {
|
|
|
6067
6219
|
const send = (message) => {
|
|
6068
6220
|
socket.send(JSON.stringify(message));
|
|
6069
6221
|
};
|
|
6222
|
+
deps.touch();
|
|
6070
6223
|
const handle = deps.broadcaster.register(send);
|
|
6071
6224
|
const session = { handle, send };
|
|
6072
6225
|
socket.on("message", (data) => {
|
|
@@ -6147,7 +6300,8 @@ ${built.error.hint ?? ""}
|
|
|
6147
6300
|
ws: services.ws,
|
|
6148
6301
|
broadcaster: services.broadcaster,
|
|
6149
6302
|
logger: services.logger,
|
|
6150
|
-
uiDistPath: services.uiDistPath
|
|
6303
|
+
uiDistPath: services.uiDistPath,
|
|
6304
|
+
touch: () => services.idleWatch.touch()
|
|
6151
6305
|
});
|
|
6152
6306
|
const listening = await listenOnFirstFreePort(app, {
|
|
6153
6307
|
host: "127.0.0.1",
|
|
@@ -6174,6 +6328,7 @@ ${built.error.hint ?? ""}
|
|
|
6174
6328
|
await new Promise((resolve6) => {
|
|
6175
6329
|
const shutdown = async (signal) => {
|
|
6176
6330
|
services.logger.info("shutting down", { signal });
|
|
6331
|
+
services.idleWatch.stop();
|
|
6177
6332
|
await services.diskWatcher.stop();
|
|
6178
6333
|
const flushed = await services.repoWriter.flushAll();
|
|
6179
6334
|
if (flushed.length > 0) {
|
|
@@ -6184,18 +6339,75 @@ ${built.error.hint ?? ""}
|
|
|
6184
6339
|
services.close();
|
|
6185
6340
|
resolve6();
|
|
6186
6341
|
};
|
|
6342
|
+
services.idleWatch.start({
|
|
6343
|
+
isWatched: () => services.broadcaster.clientCount() > 0,
|
|
6344
|
+
onIdle: () => void shutdown("idle")
|
|
6345
|
+
});
|
|
6187
6346
|
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
6188
6347
|
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
6348
|
+
services.onShutdownRequest(() => void shutdown("api"));
|
|
6189
6349
|
});
|
|
6190
6350
|
return 0;
|
|
6191
6351
|
}
|
|
6192
6352
|
};
|
|
6193
6353
|
|
|
6354
|
+
// src/interfaces/cli/commands/stop.ts
|
|
6355
|
+
var stopCommand = {
|
|
6356
|
+
name: "stop",
|
|
6357
|
+
summary: "stop the bridge running for this repo, flushing anything unsaved",
|
|
6358
|
+
detail: [
|
|
6359
|
+
"Ends the bridge for the repository containing the working directory.",
|
|
6360
|
+
"",
|
|
6361
|
+
"The bridge flushes every pending .excalidraw write, stops watching the repo, and",
|
|
6362
|
+
"clears its port file on the way out \u2014 so nothing drawn in the last two seconds is",
|
|
6363
|
+
"lost. Nothing is started; the next Claude Code session starts a fresh one."
|
|
6364
|
+
].join("\n"),
|
|
6365
|
+
async run(context) {
|
|
6366
|
+
const services = context.createClientServices({ prefix: "[stop]" });
|
|
6367
|
+
const repoRoot = await services.projectDir.findRepoRoot(context.runtime.cwd);
|
|
6368
|
+
if (!repoRoot.ok) {
|
|
6369
|
+
context.print(repoRoot.error.message);
|
|
6370
|
+
if (repoRoot.error.hint !== void 0) context.print(` ${repoRoot.error.hint}`);
|
|
6371
|
+
return 1;
|
|
6372
|
+
}
|
|
6373
|
+
const port = await services.projectDir.readPort(repoRoot.value);
|
|
6374
|
+
if (port === null) {
|
|
6375
|
+
context.print(`no bridge is running for ${repoRoot.value}`);
|
|
6376
|
+
return 0;
|
|
6377
|
+
}
|
|
6378
|
+
const health = await services.client.health(port);
|
|
6379
|
+
if (health === null) {
|
|
6380
|
+
await services.projectDir.clearPort(repoRoot.value);
|
|
6381
|
+
context.print(`nothing was listening on 127.0.0.1:${port} \u2014 cleared the stale port file`);
|
|
6382
|
+
return 0;
|
|
6383
|
+
}
|
|
6384
|
+
context.print(
|
|
6385
|
+
`stopping chalkbridge ${health.version} on 127.0.0.1:${port} (pid ${health.pid})`
|
|
6386
|
+
);
|
|
6387
|
+
if (await services.client.shutdown(port)) {
|
|
6388
|
+
context.print(" stopped \u2014 pending scene writes were flushed");
|
|
6389
|
+
return 0;
|
|
6390
|
+
}
|
|
6391
|
+
context.print(" it has no shutdown endpoint (built before v1.0.2) \u2014 ending it by pid");
|
|
6392
|
+
if (await services.launcher.stop(health.pid)) {
|
|
6393
|
+
await services.projectDir.clearPort(repoRoot.value);
|
|
6394
|
+
context.print(` stopped pid ${health.pid}`);
|
|
6395
|
+
return 0;
|
|
6396
|
+
}
|
|
6397
|
+
context.print(" it would not stop \u2014 end it yourself:");
|
|
6398
|
+
context.print(
|
|
6399
|
+
context.runtime.platform === "win32" ? ` taskkill /PID ${health.pid} /F` : ` kill ${health.pid}`
|
|
6400
|
+
);
|
|
6401
|
+
return 1;
|
|
6402
|
+
}
|
|
6403
|
+
};
|
|
6404
|
+
|
|
6194
6405
|
// src/interfaces/cli/registry.ts
|
|
6195
6406
|
var COMMANDS = [
|
|
6196
6407
|
serveCommand,
|
|
6197
6408
|
mcpCommand,
|
|
6198
6409
|
healthCommand,
|
|
6410
|
+
stopCommand,
|
|
6199
6411
|
iconsCommand
|
|
6200
6412
|
];
|
|
6201
6413
|
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createNodeSkeletonExpander, fail, isFail, isOk, ok, partition } from './chunk-
|
|
1
|
+
export { createNodeSkeletonExpander, fail, isFail, isOk, ok, partition } from './chunk-VTFAXWQJ.js';
|
|
2
2
|
import './chunk-7D4SUZUM.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
4
4
|
//# sourceMappingURL=index.js.map
|