@chinchillaenterprises/mcp-claude-channel 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -3
- package/dist/adapter.d.ts +21 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +15 -0
- package/dist/envelope.d.ts +47 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +33 -0
- package/dist/fileio.d.ts +28 -0
- package/dist/fileio.d.ts.map +1 -0
- package/dist/fileio.js +104 -0
- package/dist/fleet-board.d.ts +57 -0
- package/dist/fleet-board.d.ts.map +1 -0
- package/dist/fleet-board.js +141 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +280 -5
- package/dist/ingest.d.ts +21 -0
- package/dist/ingest.d.ts.map +1 -0
- package/dist/ingest.js +38 -0
- package/dist/policy.d.ts +52 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +115 -0
- package/dist/router.d.ts +34 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +86 -0
- package/dist/slack-adapter.d.ts +29 -0
- package/dist/slack-adapter.d.ts.map +1 -0
- package/dist/slack-adapter.js +78 -0
- package/dist/slack.d.ts +38 -0
- package/dist/slack.d.ts.map +1 -0
- package/dist/slack.js +59 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -100,6 +100,49 @@ claude --dangerously-load-development-channels server:claude-channel
|
|
|
100
100
|
}
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
## v0.2 — inbound Slack → lane router (the `--ingest` bridge)
|
|
104
|
+
|
|
105
|
+
v0.2 adds a second run mode that brings messages **from Slack into the lanes**. It runs
|
|
106
|
+
as its own resident process (`node dist/index.js --ingest`), separate from the per-lane
|
|
107
|
+
stdio server above, and keeps three layers clean:
|
|
108
|
+
|
|
109
|
+
1. **Transport / ingest (this package).** A pluggable **`InboundAdapter`** seam. Adapter
|
|
110
|
+
#1 is `SlackAdapter` — polls watched channels via the Slack Web API (reusing
|
|
111
|
+
`SLACK_BOT_TOKEN`, no public webhook), normalizes each new human message to a
|
|
112
|
+
transport-agnostic **envelope**, and stages it. A GitHub-issues webhook is the planned
|
|
113
|
+
adapter #2: same envelope, same staging log, same router — `envelope.source`
|
|
114
|
+
(`slack` | `github`) is the only distinguisher.
|
|
115
|
+
2. **Router (Coordinator-owned policy).** Reads a hot-reloaded `routing-policy.yaml`
|
|
116
|
+
(`(channel, person) → suggested_domain + suggested_lane`) — **never hardcoded**.
|
|
117
|
+
`suggested_lane` is a free-form target id that may name a bare lane (`alpha`) or a
|
|
118
|
+
**formation seat** (`squad-alpha`). The router classifies and tags the hint only.
|
|
119
|
+
3. **Delivery.** The **Coordinator (omega) is the sole receiver**: every inbound is
|
|
120
|
+
delivered to `chan-<COORDINATOR_LANE>-in.jsonl` carrying the routing hint plus the
|
|
121
|
+
**reply-back return address** (`reply_channel`, `requester`, `thread_ts`,
|
|
122
|
+
`envelope_id`) so a reply routes home even when source channel ≠ handling domain.
|
|
123
|
+
|
|
124
|
+
**Pipeline:** `adapter.collect()` → staging log `chan-inbound-raw.jsonl` (the one new
|
|
125
|
+
buffer, append-only JSONL, the proven side-door idiom) → router tails it from a journaled
|
|
126
|
+
offset (no-loss / no-double-process) → Coordinator inbox.
|
|
127
|
+
|
|
128
|
+
**Outbound mirror.** Lanes MAY reply to a Slack requester directly (low latency), but must
|
|
129
|
+
call the **`mirror_to_coordinator`** tool once per reply so the Coordinator keeps the full
|
|
130
|
+
conversation map. Lane owns the answer; Coordinator owns the routing picture.
|
|
131
|
+
|
|
132
|
+
### Ingest configuration
|
|
133
|
+
|
|
134
|
+
| Env var | Default | Notes |
|
|
135
|
+
| --- | --- | --- |
|
|
136
|
+
| `ROUTING_POLICY_PATH` | _(required)_ | Coordinator-owned routing-policy YAML (hot-reloaded). |
|
|
137
|
+
| `SLACK_BOT_TOKEN` | _(required for `--ingest`)_ | Existing bot token; the Slack poll adapter. |
|
|
138
|
+
| `LANE_INBOX_DIR` | `/tmp` | Holds the staging log, cursor bookmark, and lane inboxes. |
|
|
139
|
+
| `COORDINATOR_LANE` | `omega` | Sole-receiver lane; also the `mirror_to_coordinator` target. |
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
ROUTING_POLICY_PATH=/path/routing-policy.yaml SLACK_BOT_TOKEN=xoxb-… \
|
|
143
|
+
LANE_INBOX_DIR=/path/inboxes node dist/index.js --ingest
|
|
144
|
+
```
|
|
145
|
+
|
|
103
146
|
## Build
|
|
104
147
|
|
|
105
148
|
```bash
|
|
@@ -112,6 +155,7 @@ Published via the repo's OIDC `publish.yml`. Bump the version in **both**
|
|
|
112
155
|
|
|
113
156
|
## Roadmap
|
|
114
157
|
|
|
115
|
-
- **
|
|
116
|
-
- **
|
|
117
|
-
|
|
158
|
+
- **v0.1:** lane-to-lane nudges, file-inbox transport, no Slack.
|
|
159
|
+
- **v0.2 (this):** inbound Slack → lane router (`--ingest` bridge), pluggable
|
|
160
|
+
`InboundAdapter` seam, Coordinator-owned routing policy, `mirror_to_coordinator`.
|
|
161
|
+
- **next:** GitHub-issues webhook as adapter #2 (same envelope / staging log / router).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapter.ts — the pluggable INBOUND adapter seam.
|
|
3
|
+
*
|
|
4
|
+
* An adapter's only job: produce normalized Envelopes from some source. The staging
|
|
5
|
+
* writer, the queue (chan-inbound-raw.jsonl), and the router know NOTHING about the
|
|
6
|
+
* source — they branch only on `envelope.source` if ever needed. Slack-poll is
|
|
7
|
+
* adapter #1; a GitHub-issues webhook is adapter #2 (same envelope, same staging log,
|
|
8
|
+
* same router). New sources = a new adapter, zero changes downstream.
|
|
9
|
+
*
|
|
10
|
+
* `collect()` is pull-shaped (poll adapters return their new batch each tick). A
|
|
11
|
+
* push adapter (webhook) can implement it as "drain whatever I've buffered since the
|
|
12
|
+
* last call", keeping the bridge's tick loop uniform across adapter kinds.
|
|
13
|
+
*/
|
|
14
|
+
import type { Envelope } from "./envelope.js";
|
|
15
|
+
export interface InboundAdapter {
|
|
16
|
+
/** Source tag stamped on every envelope this adapter emits ("slack" | "github" | …). */
|
|
17
|
+
readonly source: string;
|
|
18
|
+
/** Return the batch of new normalized envelopes since the last call. */
|
|
19
|
+
collect(): Promise<Envelope[]>;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,WAAW,cAAc;IAC7B,wFAAwF;IACxF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,OAAO,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;CAChC"}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapter.ts — the pluggable INBOUND adapter seam.
|
|
3
|
+
*
|
|
4
|
+
* An adapter's only job: produce normalized Envelopes from some source. The staging
|
|
5
|
+
* writer, the queue (chan-inbound-raw.jsonl), and the router know NOTHING about the
|
|
6
|
+
* source — they branch only on `envelope.source` if ever needed. Slack-poll is
|
|
7
|
+
* adapter #1; a GitHub-issues webhook is adapter #2 (same envelope, same staging log,
|
|
8
|
+
* same router). New sources = a new adapter, zero changes downstream.
|
|
9
|
+
*
|
|
10
|
+
* `collect()` is pull-shaped (poll adapters return their new batch each tick). A
|
|
11
|
+
* push adapter (webhook) can implement it as "drain whatever I've buffered since the
|
|
12
|
+
* last call", keeping the bridge's tick loop uniform across adapter kinds.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWRhcHRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9hZGFwdGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7R0FZRyJ9
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* envelope.ts — the normalized inbound-message envelope (transport-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* Slack is adapter #1; Teams / in-app BeonIQ plug into the same shape later. The
|
|
5
|
+
* ingest leg produces everything down to `text/ts/thread_ts`; the ROUTER fills
|
|
6
|
+
* `suggested_domain` + `suggested_lane` from Coordinator-owned policy (never
|
|
7
|
+
* hardcoded). Delivery model: the Coordinator (omega) is the SOLE receiver — every
|
|
8
|
+
* inbound goes to its inbox carrying those two fields as HINTS; omega makes the
|
|
9
|
+
* actual lane dispatch.
|
|
10
|
+
*
|
|
11
|
+
* The `channel` + `requester` + `thread_ts` + `id` fields are the REPLY-BACK
|
|
12
|
+
* return address: a lane's answer routes home to the original asker even when the
|
|
13
|
+
* source channel ≠ the handling domain (the "Tuck missing a tool" Beon→echelon case).
|
|
14
|
+
*/
|
|
15
|
+
export interface Party {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
display: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ChannelRef {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
export type Domain = "beon" | "echelon" | "triage" | null;
|
|
25
|
+
export interface Envelope {
|
|
26
|
+
v: 1;
|
|
27
|
+
id: string;
|
|
28
|
+
source: string;
|
|
29
|
+
channel: ChannelRef;
|
|
30
|
+
requester: Party;
|
|
31
|
+
text: string;
|
|
32
|
+
ts: string;
|
|
33
|
+
thread_ts: string | null;
|
|
34
|
+
suggested_domain: Domain;
|
|
35
|
+
suggested_lane: string | null;
|
|
36
|
+
}
|
|
37
|
+
export declare function makeEnvelopeId(source: string, ts: string, channelId: string): string;
|
|
38
|
+
/** Build an ingest-stage envelope (domain/routed_lane left null for the router). */
|
|
39
|
+
export declare function newEnvelope(params: {
|
|
40
|
+
source: string;
|
|
41
|
+
channel: ChannelRef;
|
|
42
|
+
requester: Party;
|
|
43
|
+
text: string;
|
|
44
|
+
ts: string;
|
|
45
|
+
thread_ts?: string | null;
|
|
46
|
+
}): Envelope;
|
|
47
|
+
//# sourceMappingURL=envelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,IAAI,CAAC;AAE1D,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,CAAC,CAAC;IACL,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,UAAU,CAAC;IACpB,SAAS,EAAE,KAAK,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IAKzB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpF;AAED,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,UAAU,CAAC;IACpB,SAAS,EAAE,KAAK,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GAAG,QAAQ,CAaX"}
|
package/dist/envelope.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* envelope.ts — the normalized inbound-message envelope (transport-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* Slack is adapter #1; Teams / in-app BeonIQ plug into the same shape later. The
|
|
5
|
+
* ingest leg produces everything down to `text/ts/thread_ts`; the ROUTER fills
|
|
6
|
+
* `suggested_domain` + `suggested_lane` from Coordinator-owned policy (never
|
|
7
|
+
* hardcoded). Delivery model: the Coordinator (omega) is the SOLE receiver — every
|
|
8
|
+
* inbound goes to its inbox carrying those two fields as HINTS; omega makes the
|
|
9
|
+
* actual lane dispatch.
|
|
10
|
+
*
|
|
11
|
+
* The `channel` + `requester` + `thread_ts` + `id` fields are the REPLY-BACK
|
|
12
|
+
* return address: a lane's answer routes home to the original asker even when the
|
|
13
|
+
* source channel ≠ the handling domain (the "Tuck missing a tool" Beon→echelon case).
|
|
14
|
+
*/
|
|
15
|
+
export function makeEnvelopeId(source, ts, channelId) {
|
|
16
|
+
return `in-${source}-${ts}-${channelId}`;
|
|
17
|
+
}
|
|
18
|
+
/** Build an ingest-stage envelope (domain/routed_lane left null for the router). */
|
|
19
|
+
export function newEnvelope(params) {
|
|
20
|
+
return {
|
|
21
|
+
v: 1,
|
|
22
|
+
id: makeEnvelopeId(params.source, params.ts, params.channel.id),
|
|
23
|
+
source: params.source,
|
|
24
|
+
channel: params.channel,
|
|
25
|
+
requester: params.requester,
|
|
26
|
+
text: params.text,
|
|
27
|
+
ts: params.ts,
|
|
28
|
+
thread_ts: params.thread_ts ?? null,
|
|
29
|
+
suggested_domain: null,
|
|
30
|
+
suggested_lane: null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW52ZWxvcGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvZW52ZWxvcGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7R0FhRztBQWdDSCxNQUFNLFVBQVUsY0FBYyxDQUFDLE1BQWMsRUFBRSxFQUFVLEVBQUUsU0FBaUI7SUFDMUUsT0FBTyxNQUFNLE1BQU0sSUFBSSxFQUFFLElBQUksU0FBUyxFQUFFLENBQUM7QUFDM0MsQ0FBQztBQUVELG9GQUFvRjtBQUNwRixNQUFNLFVBQVUsV0FBVyxDQUFDLE1BTzNCO0lBQ0MsT0FBTztRQUNMLENBQUMsRUFBRSxDQUFDO1FBQ0osRUFBRSxFQUFFLGNBQWMsQ0FBQyxNQUFNLENBQUMsTUFBTSxFQUFFLE1BQU0sQ0FBQyxFQUFFLEVBQUUsTUFBTSxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7UUFDL0QsTUFBTSxFQUFFLE1BQU0sQ0FBQyxNQUFNO1FBQ3JCLE9BQU8sRUFBRSxNQUFNLENBQUMsT0FBTztRQUN2QixTQUFTLEVBQUUsTUFBTSxDQUFDLFNBQVM7UUFDM0IsSUFBSSxFQUFFLE1BQU0sQ0FBQyxJQUFJO1FBQ2pCLEVBQUUsRUFBRSxNQUFNLENBQUMsRUFBRTtRQUNiLFNBQVMsRUFBRSxNQUFNLENBQUMsU0FBUyxJQUFJLElBQUk7UUFDbkMsZ0JBQWdCLEVBQUUsSUFBSTtRQUN0QixjQUFjLEVBQUUsSUFBSTtLQUNyQixDQUFDO0FBQ0osQ0FBQyJ9
|
package/dist/fileio.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fileio.ts — append-only JSONL helpers + a resumable line tailer.
|
|
3
|
+
*
|
|
4
|
+
* The whole bus is append-only JSONL (the proven side-door idiom). The router is
|
|
5
|
+
* "just another tailer" of chan-inbound-raw.jsonl, with an offset bookmark on disk
|
|
6
|
+
* so nothing is lost or double-processed across restarts.
|
|
7
|
+
*/
|
|
8
|
+
/** Atomically append one JSON record as a line. */
|
|
9
|
+
export declare function appendLine(path: string, obj: unknown): void;
|
|
10
|
+
/** Read a tiny JSON bookmark file; return fallback if missing/corrupt. */
|
|
11
|
+
export declare function readJsonFile<T>(path: string, fallback: T): T;
|
|
12
|
+
/** Write a tiny JSON bookmark file (whole-file overwrite — these are small). */
|
|
13
|
+
export declare function writeJsonFile(path: string, obj: unknown): void;
|
|
14
|
+
/**
|
|
15
|
+
* Tail a JSONL file from a persisted byte offset, invoking `onLine` per new line.
|
|
16
|
+
* The offset is journaled to `<path>.offset` after each batch so a restart resumes
|
|
17
|
+
* exactly where it left off (no-loss, no double-process).
|
|
18
|
+
*/
|
|
19
|
+
export declare class Tailer {
|
|
20
|
+
private path;
|
|
21
|
+
private offsetPath;
|
|
22
|
+
private offset;
|
|
23
|
+
private buffer;
|
|
24
|
+
constructor(path: string, offsetPath?: string);
|
|
25
|
+
/** Drain newly-appended whole lines. Returns the number of lines processed. */
|
|
26
|
+
drain(onLine: (line: string) => void): number;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=fileio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileio.d.ts","sourceRoot":"","sources":["../src/fileio.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,mDAAmD;AACnD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,IAAI,CAO3D;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAM5D;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,IAAI,CAO9D;AAED;;;;GAIG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAM;gBAER,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;IAa7C,+EAA+E;IAC/E,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM;CAqC9C"}
|
package/dist/fileio.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fileio.ts — append-only JSONL helpers + a resumable line tailer.
|
|
3
|
+
*
|
|
4
|
+
* The whole bus is append-only JSONL (the proven side-door idiom). The router is
|
|
5
|
+
* "just another tailer" of chan-inbound-raw.jsonl, with an offset bookmark on disk
|
|
6
|
+
* so nothing is lost or double-processed across restarts.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, openSync, closeSync, statSync, readSync, writeSync, readFileSync } from "fs";
|
|
9
|
+
/** Atomically append one JSON record as a line. */
|
|
10
|
+
export function appendLine(path, obj) {
|
|
11
|
+
const fd = openSync(path, "a");
|
|
12
|
+
try {
|
|
13
|
+
writeSync(fd, JSON.stringify(obj) + "\n");
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
closeSync(fd);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Read a tiny JSON bookmark file; return fallback if missing/corrupt. */
|
|
20
|
+
export function readJsonFile(path, fallback) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Write a tiny JSON bookmark file (whole-file overwrite — these are small). */
|
|
29
|
+
export function writeJsonFile(path, obj) {
|
|
30
|
+
const fd = openSync(path, "w");
|
|
31
|
+
try {
|
|
32
|
+
writeSync(fd, JSON.stringify(obj, null, 2));
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
closeSync(fd);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Tail a JSONL file from a persisted byte offset, invoking `onLine` per new line.
|
|
40
|
+
* The offset is journaled to `<path>.offset` after each batch so a restart resumes
|
|
41
|
+
* exactly where it left off (no-loss, no double-process).
|
|
42
|
+
*/
|
|
43
|
+
export class Tailer {
|
|
44
|
+
path;
|
|
45
|
+
offsetPath;
|
|
46
|
+
offset;
|
|
47
|
+
buffer = "";
|
|
48
|
+
constructor(path, offsetPath) {
|
|
49
|
+
this.path = path;
|
|
50
|
+
this.offsetPath = offsetPath || `${path}.offset`;
|
|
51
|
+
if (!existsSync(path))
|
|
52
|
+
closeSync(openSync(path, "a"));
|
|
53
|
+
this.offset = readJsonFile(this.offsetPath, { offset: 0 }).offset;
|
|
54
|
+
// Guard against a bookmark pointing past a truncated/rotated file.
|
|
55
|
+
try {
|
|
56
|
+
if (statSync(path).size < this.offset)
|
|
57
|
+
this.offset = 0;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
this.offset = 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Drain newly-appended whole lines. Returns the number of lines processed. */
|
|
64
|
+
drain(onLine) {
|
|
65
|
+
let size;
|
|
66
|
+
try {
|
|
67
|
+
size = statSync(this.path).size;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
if (size < this.offset) {
|
|
73
|
+
this.offset = 0;
|
|
74
|
+
this.buffer = "";
|
|
75
|
+
}
|
|
76
|
+
if (size === this.offset)
|
|
77
|
+
return 0;
|
|
78
|
+
const fd = openSync(this.path, "r");
|
|
79
|
+
try {
|
|
80
|
+
const len = size - this.offset;
|
|
81
|
+
const buf = Buffer.alloc(len);
|
|
82
|
+
const read = readSync(fd, buf, 0, len, this.offset);
|
|
83
|
+
this.offset += read;
|
|
84
|
+
this.buffer += buf.toString("utf8", 0, read);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
closeSync(fd);
|
|
88
|
+
}
|
|
89
|
+
let count = 0;
|
|
90
|
+
let nl;
|
|
91
|
+
while ((nl = this.buffer.indexOf("\n")) !== -1) {
|
|
92
|
+
const line = this.buffer.slice(0, nl);
|
|
93
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
94
|
+
if (line.trim()) {
|
|
95
|
+
onLine(line);
|
|
96
|
+
count++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (count > 0)
|
|
100
|
+
writeJsonFile(this.offsetPath, { offset: this.offset });
|
|
101
|
+
return count;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmlsZWlvLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2ZpbGVpby50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7O0dBTUc7QUFFSCxPQUFPLEVBQUUsVUFBVSxFQUFFLFFBQVEsRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxTQUFTLEVBQUUsWUFBWSxFQUFFLE1BQU0sSUFBSSxDQUFDO0FBRWxHLG1EQUFtRDtBQUNuRCxNQUFNLFVBQVUsVUFBVSxDQUFDLElBQVksRUFBRSxHQUFZO0lBQ25ELE1BQU0sRUFBRSxHQUFHLFFBQVEsQ0FBQyxJQUFJLEVBQUUsR0FBRyxDQUFDLENBQUM7SUFDL0IsSUFBSSxDQUFDO1FBQ0gsU0FBUyxDQUFDLEVBQUUsRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLEdBQUcsQ0FBQyxHQUFHLElBQUksQ0FBQyxDQUFDO0lBQzVDLENBQUM7WUFBUyxDQUFDO1FBQ1QsU0FBUyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQ2hCLENBQUM7QUFDSCxDQUFDO0FBRUQsMEVBQTBFO0FBQzFFLE1BQU0sVUFBVSxZQUFZLENBQUksSUFBWSxFQUFFLFFBQVc7SUFDdkQsSUFBSSxDQUFDO1FBQ0gsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsTUFBTSxDQUFDLENBQU0sQ0FBQztJQUNyRCxDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQ1AsT0FBTyxRQUFRLENBQUM7SUFDbEIsQ0FBQztBQUNILENBQUM7QUFFRCxnRkFBZ0Y7QUFDaEYsTUFBTSxVQUFVLGFBQWEsQ0FBQyxJQUFZLEVBQUUsR0FBWTtJQUN0RCxNQUFNLEVBQUUsR0FBRyxRQUFRLENBQUMsSUFBSSxFQUFFLEdBQUcsQ0FBQyxDQUFDO0lBQy9CLElBQUksQ0FBQztRQUNILFNBQVMsQ0FBQyxFQUFFLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQyxHQUFHLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDOUMsQ0FBQztZQUFTLENBQUM7UUFDVCxTQUFTLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDaEIsQ0FBQztBQUNILENBQUM7QUFFRDs7OztHQUlHO0FBQ0gsTUFBTSxPQUFPLE1BQU07SUFDVCxJQUFJLENBQVM7SUFDYixVQUFVLENBQVM7SUFDbkIsTUFBTSxDQUFTO0lBQ2YsTUFBTSxHQUFHLEVBQUUsQ0FBQztJQUVwQixZQUFZLElBQVksRUFBRSxVQUFtQjtRQUMzQyxJQUFJLENBQUMsSUFBSSxHQUFHLElBQUksQ0FBQztRQUNqQixJQUFJLENBQUMsVUFBVSxHQUFHLFVBQVUsSUFBSSxHQUFHLElBQUksU0FBUyxDQUFDO1FBQ2pELElBQUksQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDO1lBQUUsU0FBUyxDQUFDLFFBQVEsQ0FBQyxJQUFJLEVBQUUsR0FBRyxDQUFDLENBQUMsQ0FBQztRQUN0RCxJQUFJLENBQUMsTUFBTSxHQUFHLFlBQVksQ0FBcUIsSUFBSSxDQUFDLFVBQVUsRUFBRSxFQUFFLE1BQU0sRUFBRSxDQUFDLEVBQUUsQ0FBQyxDQUFDLE1BQU0sQ0FBQztRQUN0RixtRUFBbUU7UUFDbkUsSUFBSSxDQUFDO1lBQ0gsSUFBSSxRQUFRLENBQUMsSUFBSSxDQUFDLENBQUMsSUFBSSxHQUFHLElBQUksQ0FBQyxNQUFNO2dCQUFFLElBQUksQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDO1FBQ3pELENBQUM7UUFBQyxNQUFNLENBQUM7WUFDUCxJQUFJLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQztRQUNsQixDQUFDO0lBQ0gsQ0FBQztJQUVELCtFQUErRTtJQUMvRSxLQUFLLENBQUMsTUFBOEI7UUFDbEMsSUFBSSxJQUFZLENBQUM7UUFDakIsSUFBSSxDQUFDO1lBQ0gsSUFBSSxHQUFHLFFBQVEsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUMsSUFBSSxDQUFDO1FBQ2xDLENBQUM7UUFBQyxNQUFNLENBQUM7WUFDUCxPQUFPLENBQUMsQ0FBQztRQUNYLENBQUM7UUFDRCxJQUFJLElBQUksR0FBRyxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUM7WUFDdkIsSUFBSSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUM7WUFDaEIsSUFBSSxDQUFDLE1BQU0sR0FBRyxFQUFFLENBQUM7UUFDbkIsQ0FBQztRQUNELElBQUksSUFBSSxLQUFLLElBQUksQ0FBQyxNQUFNO1lBQUUsT0FBTyxDQUFDLENBQUM7UUFFbkMsTUFBTSxFQUFFLEdBQUcsUUFBUSxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsR0FBRyxDQUFDLENBQUM7UUFDcEMsSUFBSSxDQUFDO1lBQ0gsTUFBTSxHQUFHLEdBQUcsSUFBSSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUM7WUFDL0IsTUFBTSxHQUFHLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUM5QixNQUFNLElBQUksR0FBRyxRQUFRLENBQUMsRUFBRSxFQUFFLEdBQUcsRUFBRSxDQUFDLEVBQUUsR0FBRyxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQztZQUNwRCxJQUFJLENBQUMsTUFBTSxJQUFJLElBQUksQ0FBQztZQUNwQixJQUFJLENBQUMsTUFBTSxJQUFJLEdBQUcsQ0FBQyxRQUFRLENBQUMsTUFBTSxFQUFFLENBQUMsRUFBRSxJQUFJLENBQUMsQ0FBQztRQUMvQyxDQUFDO2dCQUFTLENBQUM7WUFDVCxTQUFTLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDaEIsQ0FBQztRQUVELElBQUksS0FBSyxHQUFHLENBQUMsQ0FBQztRQUNkLElBQUksRUFBVSxDQUFDO1FBQ2YsT0FBTyxDQUFDLEVBQUUsR0FBRyxJQUFJLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUM7WUFDL0MsTUFBTSxJQUFJLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDO1lBQ3RDLElBQUksQ0FBQyxNQUFNLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDO1lBQ3hDLElBQUksSUFBSSxDQUFDLElBQUksRUFBRSxFQUFFLENBQUM7Z0JBQ2hCLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQztnQkFDYixLQUFLLEVBQUUsQ0FBQztZQUNWLENBQUM7UUFDSCxDQUFDO1FBQ0QsSUFBSSxLQUFLLEdBQUcsQ0FBQztZQUFFLGFBQWEsQ0FBQyxJQUFJLENBQUMsVUFBVSxFQUFFLEVBQUUsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO1FBQ3ZFLE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztDQUNGIn0=
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fleet-board.ts — lane presence registry writer (the MCP-tool side of the board).
|
|
3
|
+
*
|
|
4
|
+
* Backs the `clock_in` and `heartbeat` MCP tools. Writes the SAME rows/columns as
|
|
5
|
+
* the reference CLI `coordinator/scripts/clockin.sh`, so the CLI (human/ops
|
|
6
|
+
* interface) and these tools (lane interface) interoperate on one DB. Match the
|
|
7
|
+
* schema EXACTLY — see coordinator/notes/spec-fleet-board.md.
|
|
8
|
+
*
|
|
9
|
+
* DB: /Users/feather/Repos/fleet-board.db (override via FLEET_BOARD_DB). WAL mode so
|
|
10
|
+
* a lane can write its heartbeat while readers (board.sh) query. The first caller
|
|
11
|
+
* bootstraps the table + indexes (idempotent), so there is no separate init step.
|
|
12
|
+
*
|
|
13
|
+
* Uses Node's built-in `node:sqlite` (Node ≥ 22.5) — zero native deps, publish-safe.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEFAULT_BOARD_PATH = "/Users/feather/Repos/fleet-board.db";
|
|
16
|
+
export declare function boardPath(): string;
|
|
17
|
+
export interface ClockInArgs {
|
|
18
|
+
callsign: string;
|
|
19
|
+
project?: string;
|
|
20
|
+
repo_path?: string;
|
|
21
|
+
role?: string;
|
|
22
|
+
lead?: string;
|
|
23
|
+
session_id?: string;
|
|
24
|
+
pid?: number;
|
|
25
|
+
tab_name?: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
git_branch?: string;
|
|
28
|
+
status?: string;
|
|
29
|
+
loop_armed?: number;
|
|
30
|
+
host?: string;
|
|
31
|
+
protocol_version?: string;
|
|
32
|
+
event?: string;
|
|
33
|
+
notes?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface HeartbeatArgs {
|
|
36
|
+
callsign: string;
|
|
37
|
+
status?: string;
|
|
38
|
+
loop_armed?: number;
|
|
39
|
+
event?: string;
|
|
40
|
+
notes?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare class FleetBoard {
|
|
43
|
+
private db;
|
|
44
|
+
constructor(path?: string);
|
|
45
|
+
/**
|
|
46
|
+
* Boot UPSERT on callsign. `booted_at` is stamped once on insert (never updated on
|
|
47
|
+
* conflict); `last_seen_at` = now on every call. Mirrors clockin.sh defaults.
|
|
48
|
+
*/
|
|
49
|
+
clockIn(args: ClockInArgs, now?: number): void;
|
|
50
|
+
/**
|
|
51
|
+
* Heartbeat: bump last_seen_at + only the optional fields provided. Returns false
|
|
52
|
+
* if the callsign has no row (boot clock-in must run first).
|
|
53
|
+
*/
|
|
54
|
+
heartbeat(args: HeartbeatArgs, now?: number): boolean;
|
|
55
|
+
close(): void;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=fleet-board.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fleet-board.d.ts","sourceRoot":"","sources":["../src/fleet-board.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,eAAO,MAAM,kBAAkB,wCAAwC,CAAC;AAExE,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAeD,qBAAa,UAAU;IACrB,OAAO,CAAC,EAAE,CAAe;gBAEb,IAAI,SAAc;IA+B9B;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,SAAgC,GAAG,IAAI;IAuDrE;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,SAAgC,GAAG,OAAO;IAwB5E,KAAK,IAAI,IAAI;CAOd"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fleet-board.ts — lane presence registry writer (the MCP-tool side of the board).
|
|
3
|
+
*
|
|
4
|
+
* Backs the `clock_in` and `heartbeat` MCP tools. Writes the SAME rows/columns as
|
|
5
|
+
* the reference CLI `coordinator/scripts/clockin.sh`, so the CLI (human/ops
|
|
6
|
+
* interface) and these tools (lane interface) interoperate on one DB. Match the
|
|
7
|
+
* schema EXACTLY — see coordinator/notes/spec-fleet-board.md.
|
|
8
|
+
*
|
|
9
|
+
* DB: /Users/feather/Repos/fleet-board.db (override via FLEET_BOARD_DB). WAL mode so
|
|
10
|
+
* a lane can write its heartbeat while readers (board.sh) query. The first caller
|
|
11
|
+
* bootstraps the table + indexes (idempotent), so there is no separate init step.
|
|
12
|
+
*
|
|
13
|
+
* Uses Node's built-in `node:sqlite` (Node ≥ 22.5) — zero native deps, publish-safe.
|
|
14
|
+
*/
|
|
15
|
+
import { DatabaseSync } from "node:sqlite";
|
|
16
|
+
import { hostname } from "os";
|
|
17
|
+
import { execFileSync } from "child_process";
|
|
18
|
+
export const DEFAULT_BOARD_PATH = "/Users/feather/Repos/fleet-board.db";
|
|
19
|
+
export function boardPath() {
|
|
20
|
+
return process.env.FLEET_BOARD_DB || DEFAULT_BOARD_PATH;
|
|
21
|
+
}
|
|
22
|
+
/** Derive the git branch of a repo path (best-effort; "" if not a repo). */
|
|
23
|
+
function deriveBranch(repoPath) {
|
|
24
|
+
if (!repoPath)
|
|
25
|
+
return "";
|
|
26
|
+
try {
|
|
27
|
+
return execFileSync("git", ["-C", repoPath, "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
30
|
+
}).trim();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class FleetBoard {
|
|
37
|
+
db;
|
|
38
|
+
constructor(path = boardPath()) {
|
|
39
|
+
this.db = new DatabaseSync(path);
|
|
40
|
+
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
41
|
+
this.db.exec("PRAGMA busy_timeout = 5000;");
|
|
42
|
+
// Schema MUST match clockin.sh exactly so CLI + MCP rows interoperate.
|
|
43
|
+
this.db.exec(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS lanes (
|
|
45
|
+
callsign TEXT PRIMARY KEY,
|
|
46
|
+
session_id TEXT,
|
|
47
|
+
project TEXT,
|
|
48
|
+
repo_path TEXT,
|
|
49
|
+
role TEXT,
|
|
50
|
+
lead TEXT,
|
|
51
|
+
host TEXT,
|
|
52
|
+
pid INTEGER,
|
|
53
|
+
tab_name TEXT,
|
|
54
|
+
status TEXT DEFAULT 'alive',
|
|
55
|
+
booted_at INTEGER,
|
|
56
|
+
last_seen_at INTEGER,
|
|
57
|
+
loop_armed INTEGER,
|
|
58
|
+
model TEXT,
|
|
59
|
+
git_branch TEXT,
|
|
60
|
+
protocol_version TEXT,
|
|
61
|
+
last_event TEXT,
|
|
62
|
+
notes TEXT
|
|
63
|
+
);
|
|
64
|
+
`);
|
|
65
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_lanes_project_status ON lanes(project, status);");
|
|
66
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_lanes_last_seen ON lanes(last_seen_at);");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Boot UPSERT on callsign. `booted_at` is stamped once on insert (never updated on
|
|
70
|
+
* conflict); `last_seen_at` = now on every call. Mirrors clockin.sh defaults.
|
|
71
|
+
*/
|
|
72
|
+
clockIn(args, now = Math.floor(Date.now() / 1000)) {
|
|
73
|
+
const host = args.host && args.host.trim() ? args.host : hostname();
|
|
74
|
+
const lead = args.lead && args.lead.trim() ? args.lead : "omega";
|
|
75
|
+
const status = args.status && args.status.trim() ? args.status : "alive";
|
|
76
|
+
const branch = args.git_branch && args.git_branch.trim() ? args.git_branch : deriveBranch(args.repo_path);
|
|
77
|
+
const loopArmed = args.loop_armed === 1 || args.loop_armed === true ? 1 : 0;
|
|
78
|
+
this.db
|
|
79
|
+
.prepare(`INSERT INTO lanes (
|
|
80
|
+
callsign, session_id, project, repo_path, role, lead, host, pid, tab_name,
|
|
81
|
+
status, booted_at, last_seen_at, loop_armed, model, git_branch,
|
|
82
|
+
protocol_version, last_event, notes
|
|
83
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
84
|
+
ON CONFLICT(callsign) DO UPDATE SET
|
|
85
|
+
session_id = excluded.session_id,
|
|
86
|
+
project = excluded.project,
|
|
87
|
+
repo_path = excluded.repo_path,
|
|
88
|
+
role = excluded.role,
|
|
89
|
+
lead = excluded.lead,
|
|
90
|
+
host = excluded.host,
|
|
91
|
+
pid = excluded.pid,
|
|
92
|
+
tab_name = excluded.tab_name,
|
|
93
|
+
status = excluded.status,
|
|
94
|
+
last_seen_at = excluded.last_seen_at,
|
|
95
|
+
loop_armed = excluded.loop_armed,
|
|
96
|
+
model = excluded.model,
|
|
97
|
+
git_branch = excluded.git_branch,
|
|
98
|
+
protocol_version = excluded.protocol_version,
|
|
99
|
+
last_event = excluded.last_event,
|
|
100
|
+
notes = excluded.notes`)
|
|
101
|
+
.run(args.callsign, args.session_id ?? "", args.project ?? "", args.repo_path ?? "", args.role ?? "", lead, host, args.pid ?? null, args.tab_name ?? "", status, now, // booted_at (insert only — DO UPDATE omits it)
|
|
102
|
+
now, // last_seen_at
|
|
103
|
+
loopArmed, args.model ?? "", branch, args.protocol_version ?? "", args.event ?? "", args.notes ?? "");
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Heartbeat: bump last_seen_at + only the optional fields provided. Returns false
|
|
107
|
+
* if the callsign has no row (boot clock-in must run first).
|
|
108
|
+
*/
|
|
109
|
+
heartbeat(args, now = Math.floor(Date.now() / 1000)) {
|
|
110
|
+
const sets = ["last_seen_at = ?"];
|
|
111
|
+
const vals = [now];
|
|
112
|
+
if (args.status !== undefined && String(args.status).trim()) {
|
|
113
|
+
sets.push("status = ?");
|
|
114
|
+
vals.push(args.status);
|
|
115
|
+
}
|
|
116
|
+
if (args.loop_armed !== undefined) {
|
|
117
|
+
sets.push("loop_armed = ?");
|
|
118
|
+
vals.push(args.loop_armed === 1 || args.loop_armed === true ? 1 : 0);
|
|
119
|
+
}
|
|
120
|
+
if (args.event !== undefined && String(args.event).trim()) {
|
|
121
|
+
sets.push("last_event = ?");
|
|
122
|
+
vals.push(args.event);
|
|
123
|
+
}
|
|
124
|
+
if (args.notes !== undefined && String(args.notes).trim()) {
|
|
125
|
+
sets.push("notes = ?");
|
|
126
|
+
vals.push(args.notes);
|
|
127
|
+
}
|
|
128
|
+
vals.push(args.callsign);
|
|
129
|
+
const res = this.db.prepare(`UPDATE lanes SET ${sets.join(", ")} WHERE callsign = ?`).run(...vals);
|
|
130
|
+
return Number(res.changes) > 0;
|
|
131
|
+
}
|
|
132
|
+
close() {
|
|
133
|
+
try {
|
|
134
|
+
this.db.close();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* ignore */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmxlZXQtYm9hcmQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvZmxlZXQtYm9hcmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7R0FhRztBQUVILE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDM0MsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLElBQUksQ0FBQztBQUM5QixPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sZUFBZSxDQUFDO0FBRTdDLE1BQU0sQ0FBQyxNQUFNLGtCQUFrQixHQUFHLHFDQUFxQyxDQUFDO0FBRXhFLE1BQU0sVUFBVSxTQUFTO0lBQ3ZCLE9BQU8sT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLElBQUksa0JBQWtCLENBQUM7QUFDMUQsQ0FBQztBQTZCRCw0RUFBNEU7QUFDNUUsU0FBUyxZQUFZLENBQUMsUUFBaUI7SUFDckMsSUFBSSxDQUFDLFFBQVE7UUFBRSxPQUFPLEVBQUUsQ0FBQztJQUN6QixJQUFJLENBQUM7UUFDSCxPQUFPLFlBQVksQ0FBQyxLQUFLLEVBQUUsQ0FBQyxJQUFJLEVBQUUsUUFBUSxFQUFFLFdBQVcsRUFBRSxjQUFjLEVBQUUsTUFBTSxDQUFDLEVBQUU7WUFDaEYsUUFBUSxFQUFFLE1BQU07WUFDaEIsS0FBSyxFQUFFLENBQUMsUUFBUSxFQUFFLE1BQU0sRUFBRSxRQUFRLENBQUM7U0FDcEMsQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDO0lBQ1osQ0FBQztJQUFDLE1BQU0sQ0FBQztRQUNQLE9BQU8sRUFBRSxDQUFDO0lBQ1osQ0FBQztBQUNILENBQUM7QUFFRCxNQUFNLE9BQU8sVUFBVTtJQUNiLEVBQUUsQ0FBZTtJQUV6QixZQUFZLElBQUksR0FBRyxTQUFTLEVBQUU7UUFDNUIsSUFBSSxDQUFDLEVBQUUsR0FBRyxJQUFJLFlBQVksQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUNqQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyw0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
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ declare class ClaudeChannelServer {
|
|
|
37
37
|
private offset;
|
|
38
38
|
private buffer;
|
|
39
39
|
private pollTimer;
|
|
40
|
+
private board;
|
|
40
41
|
constructor();
|
|
41
42
|
/**
|
|
42
43
|
* Emit a `notifications/claude/channel` push. Each `meta` key becomes an
|
|
@@ -51,6 +52,10 @@ declare class ClaudeChannelServer {
|
|
|
51
52
|
/** Tail our own inbox file; push each newly-appended line. */
|
|
52
53
|
private startInboxTailer;
|
|
53
54
|
private sendToLane;
|
|
55
|
+
private mirrorToCoordinator;
|
|
56
|
+
private getBoard;
|
|
57
|
+
private clockIn;
|
|
58
|
+
private heartbeat;
|
|
54
59
|
private setupHandlers;
|
|
55
60
|
run(): Promise<void>;
|
|
56
61
|
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AA4HH,cAAM,mBAAmB;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAqC;IACtD,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,KAAK,CAA2B;;IAqBxC;;;;;;OAMG;YACW,aAAa;IAqB3B,6EAA6E;YAC/D,eAAe;IA8B7B,8DAA8D;IAC9D,OAAO,CAAC,gBAAgB;YAsDV,UAAU;YAwCV,mBAAmB;IAwCjC,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,OAAO;IAwBf,OAAO,CAAC,SAAS;IAiCjB,OAAO,CAAC,aAAa;IAgHf,GAAG;CAgBV;AAkGD,OAAO,EAAE,mBAAmB,EAAE,CAAC"}
|