@tidegap/cli 0.0.0-stage → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +125 -2
- package/dist/commands/env-file.d.ts +53 -0
- package/dist/commands/env-file.d.ts.map +1 -0
- package/dist/commands/env-file.js +127 -0
- package/dist/commands/env-file.js.map +1 -0
- package/dist/commands/install.d.ts +84 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +299 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/queue.d.ts +139 -0
- package/dist/commands/queue.d.ts.map +1 -0
- package/dist/commands/queue.js +491 -0
- package/dist/commands/queue.js.map +1 -0
- package/dist/commands/receipts.d.ts +15 -0
- package/dist/commands/receipts.d.ts.map +1 -0
- package/dist/commands/receipts.js +70 -0
- package/dist/commands/receipts.js.map +1 -0
- package/dist/commands/register-wake.d.ts +21 -0
- package/dist/commands/register-wake.d.ts.map +1 -0
- package/dist/commands/register-wake.js +76 -0
- package/dist/commands/register-wake.js.map +1 -0
- package/dist/commands/stats.d.ts +31 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +165 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/commands/tick.d.ts +73 -0
- package/dist/commands/tick.d.ts.map +1 -0
- package/dist/commands/tick.js +294 -0
- package/dist/commands/tick.js.map +1 -0
- package/dist/commands/ui.d.ts +25 -0
- package/dist/commands/ui.d.ts.map +1 -0
- package/dist/commands/ui.js +83 -0
- package/dist/commands/ui.js.map +1 -0
- package/dist/commands/verify.d.ts +32 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +135 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/env.d.ts +55 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +90 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +351 -0
- package/dist/index.js.map +1 -0
- package/dist/platform/index.d.ts +30 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +66 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/linux.d.ts +81 -0
- package/dist/platform/linux.d.ts.map +1 -0
- package/dist/platform/linux.js +158 -0
- package/dist/platform/linux.js.map +1 -0
- package/dist/platform/macos.d.ts +69 -0
- package/dist/platform/macos.d.ts.map +1 -0
- package/dist/platform/macos.js +151 -0
- package/dist/platform/macos.js.map +1 -0
- package/dist/platform/windows.d.ts +31 -0
- package/dist/platform/windows.d.ts.map +1 -0
- package/dist/platform/windows.js +58 -0
- package/dist/platform/windows.js.map +1 -0
- package/dist/ui/api.d.ts +59 -0
- package/dist/ui/api.d.ts.map +1 -0
- package/dist/ui/api.js +459 -0
- package/dist/ui/api.js.map +1 -0
- package/dist/ui/dispatcher.d.ts +84 -0
- package/dist/ui/dispatcher.d.ts.map +1 -0
- package/dist/ui/dispatcher.js +150 -0
- package/dist/ui/dispatcher.js.map +1 -0
- package/dist/ui/events.d.ts +27 -0
- package/dist/ui/events.d.ts.map +1 -0
- package/dist/ui/events.js +85 -0
- package/dist/ui/events.js.map +1 -0
- package/dist/ui/server.d.ts +69 -0
- package/dist/ui/server.d.ts.map +1 -0
- package/dist/ui/server.js +258 -0
- package/dist/ui/server.js.map +1 -0
- package/package.json +54 -3
- package/ui/agenda.js +256 -0
- package/ui/api.js +111 -0
- package/ui/app.css +707 -0
- package/ui/app.js +597 -0
- package/ui/detail.js +431 -0
- package/ui/dialog.js +279 -0
- package/ui/dnd.js +131 -0
- package/ui/dom.js +255 -0
- package/ui/index.html +37 -0
- package/ui/journal.js +207 -0
- package/ui/keys.js +92 -0
- package/ui/row.js +153 -0
- package/ui/state.js +412 -0
- package/ui/timeline.js +202 -0
- package/ui/toast.js +69 -0
- package/ui/tokens.css +106 -0
- package/ui/views.js +31 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatcher state and "Start dispatcher" for `tidegap ui` (ui v2, KTD13).
|
|
3
|
+
*
|
|
4
|
+
* The UI server never dispatches a task. When no dispatcher has run lately,
|
|
5
|
+
* the page can ask the server to start one: the same `tidegap` binary the
|
|
6
|
+
* install step resolves, spawned detached as `tidegap tick --daemon --interval
|
|
7
|
+
* 60 --db <ledger>`, with stdout and stderr appended to
|
|
8
|
+
* `~/.tidegap/logs/ui-tick.log` (directory 0700, file 0600). The child's
|
|
9
|
+
* pid, log path and start time are kept in ledger meta `ui_dispatcher`. A
|
|
10
|
+
* start is refused (409) while that child is alive or while any other
|
|
11
|
+
* ticker ticked within two intervals. The user stops it with `kill <pid>`.
|
|
12
|
+
*
|
|
13
|
+
* State: `running` when the UI-started child is alive (its pid exists and it
|
|
14
|
+
* started or ticked recently, so a recycled pid is not trusted) or a tick ran within
|
|
15
|
+
* two intervals (300 s when the daemon is installed, else 60 s); `stale`
|
|
16
|
+
* when a tick ran before that; `never` when no tick was ever recorded.
|
|
17
|
+
*/
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
import { chmodSync, closeSync, fchmodSync, mkdirSync, openSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { LAST_TICK_META_KEY, resolveTidegapHome } from "@tidegap/core";
|
|
22
|
+
export const UI_DISPATCHER_META_KEY = "ui_dispatcher";
|
|
23
|
+
/** Tick interval of a dispatcher started from the page, in seconds. */
|
|
24
|
+
export const UI_DISPATCHER_INTERVAL_S = 60;
|
|
25
|
+
/** Interval of the installed daemon (`tidegap install` default), in seconds. */
|
|
26
|
+
export const DAEMON_INTERVAL_S = 300;
|
|
27
|
+
export function defaultLogDir() {
|
|
28
|
+
return join(resolveTidegapHome(), "logs");
|
|
29
|
+
}
|
|
30
|
+
/** True when a process with `pid` exists (EPERM still means it exists). */
|
|
31
|
+
export function pidAlive(pid) {
|
|
32
|
+
try {
|
|
33
|
+
process.kill(pid, 0);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
return err.code === "EPERM";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export const defaultSpawner = (argv, logPath) => {
|
|
41
|
+
const fd = openSync(logPath, "a", 0o600);
|
|
42
|
+
try {
|
|
43
|
+
// The mode above applies only when the file is created.
|
|
44
|
+
fchmodSync(fd, 0o600);
|
|
45
|
+
const child = spawn(argv[0], argv.slice(1), {
|
|
46
|
+
detached: true,
|
|
47
|
+
stdio: ["ignore", fd, fd],
|
|
48
|
+
});
|
|
49
|
+
child.on("error", () => {
|
|
50
|
+
// A launch error surfaces as a missing pid below; nothing else to do.
|
|
51
|
+
});
|
|
52
|
+
if (child.pid === undefined)
|
|
53
|
+
throw new Error("could not start the dispatcher");
|
|
54
|
+
child.unref();
|
|
55
|
+
return child.pid;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
closeSync(fd);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
export class DispatcherControl {
|
|
62
|
+
store;
|
|
63
|
+
opts;
|
|
64
|
+
constructor(store, opts) {
|
|
65
|
+
this.store = store;
|
|
66
|
+
this.opts = opts;
|
|
67
|
+
}
|
|
68
|
+
meta() {
|
|
69
|
+
const raw = this.store.getMeta(UI_DISPATCHER_META_KEY);
|
|
70
|
+
if (!raw)
|
|
71
|
+
return undefined;
|
|
72
|
+
try {
|
|
73
|
+
const m = JSON.parse(raw);
|
|
74
|
+
if (typeof m.pid !== "number" || typeof m.log !== "string" || typeof m.started_at !== "string") {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return m;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
alive(pid) {
|
|
84
|
+
return (this.opts.isAlive ?? pidAlive)(pid);
|
|
85
|
+
}
|
|
86
|
+
state() {
|
|
87
|
+
const meta = this.meta();
|
|
88
|
+
const lastTick = this.store.getMeta(LAST_TICK_META_KEY) ?? null;
|
|
89
|
+
const interval = this.opts.daemonStatus() === "installed" ? DAEMON_INTERVAL_S : UI_DISPATCHER_INTERVAL_S;
|
|
90
|
+
const now = (this.opts.now?.() ?? new Date()).getTime();
|
|
91
|
+
const window = 2 * interval * 1000;
|
|
92
|
+
const recent = lastTick !== null && now - Date.parse(lastTick) <= window;
|
|
93
|
+
// A live pid alone is not proof: after a crash or reboot the OS can give
|
|
94
|
+
// the pid to another process. The child counts as alive while its pid
|
|
95
|
+
// exists AND it either started recently or a tick ran recently.
|
|
96
|
+
const child = meta
|
|
97
|
+
? {
|
|
98
|
+
...meta,
|
|
99
|
+
alive: this.alive(meta.pid) &&
|
|
100
|
+
(recent ||
|
|
101
|
+
now - Date.parse(meta.started_at) <= 2 * UI_DISPATCHER_INTERVAL_S * 1000),
|
|
102
|
+
}
|
|
103
|
+
: null;
|
|
104
|
+
const state = child?.alive || recent ? "running" : lastTick !== null ? "stale" : "never";
|
|
105
|
+
return { state, last_tick_at: lastTick, interval_s: interval, ui_child: child };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Spawn the dispatcher. Refused (`ok: false`) when the child started from
|
|
109
|
+
* the page is alive (`ui_child`), or when another ticker (the installed
|
|
110
|
+
* daemon, an `tidegap tick` loop, OpenClaw) ticked recently (`ticking`): a
|
|
111
|
+
* second ticker would only race the first.
|
|
112
|
+
*/
|
|
113
|
+
start() {
|
|
114
|
+
const current = this.state();
|
|
115
|
+
if (current.ui_child?.alive) {
|
|
116
|
+
const { alive: _alive, ...meta } = current.ui_child;
|
|
117
|
+
return { ok: false, reason: "ui_child", meta };
|
|
118
|
+
}
|
|
119
|
+
if (current.state === "running") {
|
|
120
|
+
return { ok: false, reason: "ticking", last_tick_at: current.last_tick_at };
|
|
121
|
+
}
|
|
122
|
+
const logDir = this.opts.logDir ?? defaultLogDir();
|
|
123
|
+
mkdirSync(logDir, { recursive: true, mode: 0o700 });
|
|
124
|
+
try {
|
|
125
|
+
chmodSync(logDir, 0o700);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// best-effort on exotic filesystems
|
|
129
|
+
}
|
|
130
|
+
const log = join(logDir, "ui-tick.log");
|
|
131
|
+
const argv = [
|
|
132
|
+
...this.opts.launcher,
|
|
133
|
+
"tick",
|
|
134
|
+
"--daemon",
|
|
135
|
+
"--interval",
|
|
136
|
+
String(UI_DISPATCHER_INTERVAL_S),
|
|
137
|
+
"--db",
|
|
138
|
+
this.opts.dbPath,
|
|
139
|
+
];
|
|
140
|
+
const pid = (this.opts.spawn ?? defaultSpawner)(argv, log);
|
|
141
|
+
const meta = {
|
|
142
|
+
pid,
|
|
143
|
+
log,
|
|
144
|
+
started_at: (this.opts.now?.() ?? new Date()).toISOString(),
|
|
145
|
+
};
|
|
146
|
+
this.store.setMeta(UI_DISPATCHER_META_KEY, JSON.stringify(meta));
|
|
147
|
+
return { ok: true, meta };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=dispatcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatcher.js","sourceRoot":"","sources":["../../src/ui/dispatcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Change feed for `tidegap ui` (KTD10). Polls `PRAGMA data_version`, which moves
|
|
3
|
+
* when another connection commits (the tick, the MCP server). On a move it
|
|
4
|
+
* re-lists the ledger and reports the ids whose status, read marker or
|
|
5
|
+
* scheduled time changed. The server's own writes do not move
|
|
6
|
+
* `data_version`, so the server calls `emit` itself after a mark-read.
|
|
7
|
+
*/
|
|
8
|
+
import type { TaskStore } from "@tidegap/core";
|
|
9
|
+
export type TaskChangeListener = (ids: string[]) => void;
|
|
10
|
+
export declare class TaskEvents {
|
|
11
|
+
private readonly store;
|
|
12
|
+
private readonly intervalMs;
|
|
13
|
+
private readonly listeners;
|
|
14
|
+
private fingerprints;
|
|
15
|
+
private version;
|
|
16
|
+
private timer;
|
|
17
|
+
constructor(store: TaskStore, intervalMs?: number);
|
|
18
|
+
start(): void;
|
|
19
|
+
stop(): void;
|
|
20
|
+
subscribe(fn: TaskChangeListener): () => void;
|
|
21
|
+
/** Push a change the poller cannot see (the server's own write). */
|
|
22
|
+
emit(ids: string[]): void;
|
|
23
|
+
/** One poll step; public so tests need not wait on the timer. */
|
|
24
|
+
poll(): void;
|
|
25
|
+
private snapshot;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/ui/events.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;AAEzD,qBAAa,UAAU;IAOnB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAP7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAC3D,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,KAAK,CAA6B;gBAGvB,KAAK,EAAE,SAAS,EAChB,UAAU,SAAQ;IAMrC,KAAK,IAAI,IAAI;IAMb,IAAI,IAAI,IAAI;IAMZ,SAAS,CAAC,EAAE,EAAE,kBAAkB,GAAG,MAAM,IAAI;IAK7C,oEAAoE;IACpE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI;IAOzB,iEAAiE;IACjE,IAAI,IAAI,IAAI;IAuBZ,OAAO,CAAC,QAAQ;CAUjB"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Change feed for `tidegap ui` (KTD10). Polls `PRAGMA data_version`, which moves
|
|
3
|
+
* when another connection commits (the tick, the MCP server). On a move it
|
|
4
|
+
* re-lists the ledger and reports the ids whose status, read marker or
|
|
5
|
+
* scheduled time changed. The server's own writes do not move
|
|
6
|
+
* `data_version`, so the server calls `emit` itself after a mark-read.
|
|
7
|
+
*/
|
|
8
|
+
export class TaskEvents {
|
|
9
|
+
store;
|
|
10
|
+
intervalMs;
|
|
11
|
+
listeners = new Set();
|
|
12
|
+
fingerprints;
|
|
13
|
+
version;
|
|
14
|
+
timer;
|
|
15
|
+
constructor(store, intervalMs = 2_000) {
|
|
16
|
+
this.store = store;
|
|
17
|
+
this.intervalMs = intervalMs;
|
|
18
|
+
this.version = store.dataVersion();
|
|
19
|
+
this.fingerprints = this.snapshot();
|
|
20
|
+
}
|
|
21
|
+
start() {
|
|
22
|
+
if (this.timer)
|
|
23
|
+
return;
|
|
24
|
+
this.timer = setInterval(() => this.poll(), this.intervalMs);
|
|
25
|
+
this.timer.unref();
|
|
26
|
+
}
|
|
27
|
+
stop() {
|
|
28
|
+
if (this.timer)
|
|
29
|
+
clearInterval(this.timer);
|
|
30
|
+
this.timer = undefined;
|
|
31
|
+
this.listeners.clear();
|
|
32
|
+
}
|
|
33
|
+
subscribe(fn) {
|
|
34
|
+
this.listeners.add(fn);
|
|
35
|
+
return () => this.listeners.delete(fn);
|
|
36
|
+
}
|
|
37
|
+
/** Push a change the poller cannot see (the server's own write). */
|
|
38
|
+
emit(ids) {
|
|
39
|
+
// Fingerprints stay as they are: re-reading them here could absorb a
|
|
40
|
+
// tick commit that landed between the write and the next poll. The next
|
|
41
|
+
// external commit may report these ids once more, which is harmless.
|
|
42
|
+
for (const fn of this.listeners)
|
|
43
|
+
fn(ids);
|
|
44
|
+
}
|
|
45
|
+
/** One poll step; public so tests need not wait on the timer. */
|
|
46
|
+
poll() {
|
|
47
|
+
let v;
|
|
48
|
+
try {
|
|
49
|
+
v = this.store.dataVersion();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (v === this.version)
|
|
55
|
+
return;
|
|
56
|
+
let next;
|
|
57
|
+
try {
|
|
58
|
+
next = this.snapshot();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// A busy or locked ledger: keep the old version so the next poll retries.
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.version = v;
|
|
65
|
+
const changed = [];
|
|
66
|
+
for (const [id, fp] of next)
|
|
67
|
+
if (this.fingerprints.get(id) !== fp)
|
|
68
|
+
changed.push(id);
|
|
69
|
+
for (const id of this.fingerprints.keys())
|
|
70
|
+
if (!next.has(id))
|
|
71
|
+
changed.push(id);
|
|
72
|
+
this.fingerprints = next;
|
|
73
|
+
if (changed.length > 0)
|
|
74
|
+
for (const fn of this.listeners)
|
|
75
|
+
fn(changed);
|
|
76
|
+
}
|
|
77
|
+
snapshot() {
|
|
78
|
+
const out = new Map();
|
|
79
|
+
for (const r of this.store.list()) {
|
|
80
|
+
out.set(r.taskId, `${r.status}|${r.readAt ?? ""}|${r.scheduledFor ?? ""}|${r.deadline ?? ""}|${r.repeat ?? ""}`);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/ui/events.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tidegap ui` HTTP server (KTD8, KTD9). Loopback only. Writes by POST, never
|
|
3
|
+
* dispatches: actions go through a Scheduler with no adapters, and the
|
|
4
|
+
* dispatcher (`tidegap tick` or the OpenClaw loop) runs the task.
|
|
5
|
+
*
|
|
6
|
+
* Security model:
|
|
7
|
+
* - binds 127.0.0.1 only; there is no host option;
|
|
8
|
+
* - every `/api/*` request (JSON and SSE) needs `Authorization: Bearer
|
|
9
|
+
* <token>`; the token is random per run and compared in constant time;
|
|
10
|
+
* - a `Host` header whose name is not 127.0.0.1, localhost or [::1] (with
|
|
11
|
+
* no port or this server's port) gets 403, which blocks DNS-rebinding
|
|
12
|
+
* pages; an `Origin` header, when sent, must name the same loopback
|
|
13
|
+
* host, so a page on another origin gets 403 too;
|
|
14
|
+
* - no CORS headers, so other origins cannot read responses;
|
|
15
|
+
* - static files come from a fixed map; no request path touches the disk;
|
|
16
|
+
* - every write is a POST with the bearer header; routes that read a body
|
|
17
|
+
* (cancel, reschedule, reply) need `Content-Type: application/json`
|
|
18
|
+
* (415 otherwise: a cross-site form cannot send it without a preflight,
|
|
19
|
+
* which gets no CORS answer) and at most 64 KiB (413); body-less routes
|
|
20
|
+
* (read, unread, run-now, dispatcher/start) refuse a non-empty body
|
|
21
|
+
* (400); bodies are strict JSON objects. No cookie is ever set.
|
|
22
|
+
* The static page holds no data; it reads the token from the URL fragment,
|
|
23
|
+
* which browsers never send to the server.
|
|
24
|
+
*/
|
|
25
|
+
import { type Server } from "node:http";
|
|
26
|
+
import { type ApiContext } from "./api.js";
|
|
27
|
+
import { type Spawner } from "./dispatcher.js";
|
|
28
|
+
import { TaskEvents } from "./events.js";
|
|
29
|
+
/** Largest POST body read, in bytes. */
|
|
30
|
+
export declare const MAX_BODY_BYTES: number;
|
|
31
|
+
export declare const LOOPBACK = "127.0.0.1";
|
|
32
|
+
/** ES modules of the page (ui v2). Each is listed: no path touches the disk. */
|
|
33
|
+
export declare const UI_MODULES: readonly ["app.js", "api.js", "state.js", "toast.js", "dom.js", "views.js", "row.js", "agenda.js", "detail.js", "journal.js", "timeline.js", "dnd.js", "keys.js", "dialog.js"];
|
|
34
|
+
export declare const STATIC_FILES: Readonly<Record<string, {
|
|
35
|
+
file: string;
|
|
36
|
+
type: string;
|
|
37
|
+
}>>;
|
|
38
|
+
/** `packages/cli/ui`, from both `src/ui` (tests) and `dist/ui` (published). */
|
|
39
|
+
export declare function defaultAssetDir(): string;
|
|
40
|
+
export interface UiServerOptions extends Omit<ApiContext, "store" | "dispatcher"> {
|
|
41
|
+
token: string;
|
|
42
|
+
/** `tidegap` launcher for Start dispatcher; default: the install step's. */
|
|
43
|
+
launcher?: string[];
|
|
44
|
+
/** Inject the dispatcher spawner, liveness probe and log dir (tests). */
|
|
45
|
+
spawn?: Spawner;
|
|
46
|
+
isAlive?: (pid: number) => boolean;
|
|
47
|
+
logDir?: string;
|
|
48
|
+
/** 0 (default) picks a free port. */
|
|
49
|
+
port?: number;
|
|
50
|
+
assetDir?: string;
|
|
51
|
+
/** data_version poll interval for the change stream. */
|
|
52
|
+
pollMs?: number;
|
|
53
|
+
}
|
|
54
|
+
export interface UiServer {
|
|
55
|
+
port: number;
|
|
56
|
+
address: string;
|
|
57
|
+
url: string;
|
|
58
|
+
server: Server;
|
|
59
|
+
events: TaskEvents;
|
|
60
|
+
close(): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* True when `host` (a Host header, or the host part of an Origin) names a
|
|
64
|
+
* loopback address with no port or with `port`. Anything else, including a
|
|
65
|
+
* DNS name that resolves to 127.0.0.1, is refused.
|
|
66
|
+
*/
|
|
67
|
+
export declare function isLoopbackHost(host: string, port: number): boolean;
|
|
68
|
+
export declare function startUiServer(opts: UiServerOptions): Promise<UiServer>;
|
|
69
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/ui/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AASjG,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AACpE,OAAO,EAAqB,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,wCAAwC;AACxC,eAAO,MAAM,cAAc,QAAY,CAAC;AAExC,eAAO,MAAM,QAAQ,cAAc,CAAC;AAapC,gFAAgF;AAChF,eAAO,MAAM,UAAU,gLAeb,CAAC;AAEX,eAAO,MAAM,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAKjF,CAAC;AAEF,+EAA+E;AAC/E,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,MAAM,WAAW,eACf,SAAQ,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,YAAY,CAAC;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,yEAAyE;IACzE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAQD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAIlE;AAmDD,wBAAsB,aAAa,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAiH5E"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tidegap ui` HTTP server (KTD8, KTD9). Loopback only. Writes by POST, never
|
|
3
|
+
* dispatches: actions go through a Scheduler with no adapters, and the
|
|
4
|
+
* dispatcher (`tidegap tick` or the OpenClaw loop) runs the task.
|
|
5
|
+
*
|
|
6
|
+
* Security model:
|
|
7
|
+
* - binds 127.0.0.1 only; there is no host option;
|
|
8
|
+
* - every `/api/*` request (JSON and SSE) needs `Authorization: Bearer
|
|
9
|
+
* <token>`; the token is random per run and compared in constant time;
|
|
10
|
+
* - a `Host` header whose name is not 127.0.0.1, localhost or [::1] (with
|
|
11
|
+
* no port or this server's port) gets 403, which blocks DNS-rebinding
|
|
12
|
+
* pages; an `Origin` header, when sent, must name the same loopback
|
|
13
|
+
* host, so a page on another origin gets 403 too;
|
|
14
|
+
* - no CORS headers, so other origins cannot read responses;
|
|
15
|
+
* - static files come from a fixed map; no request path touches the disk;
|
|
16
|
+
* - every write is a POST with the bearer header; routes that read a body
|
|
17
|
+
* (cancel, reschedule, reply) need `Content-Type: application/json`
|
|
18
|
+
* (415 otherwise: a cross-site form cannot send it without a preflight,
|
|
19
|
+
* which gets no CORS answer) and at most 64 KiB (413); body-less routes
|
|
20
|
+
* (read, unread, run-now, dispatcher/start) refuse a non-empty body
|
|
21
|
+
* (400); bodies are strict JSON objects. No cookie is ever set.
|
|
22
|
+
* The static page holds no data; it reads the token from the URL fragment,
|
|
23
|
+
* which browsers never send to the server.
|
|
24
|
+
*/
|
|
25
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { createServer } from "node:http";
|
|
28
|
+
import { dirname, join } from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
import { TaskStore } from "@tidegap/core";
|
|
31
|
+
import { resolveLauncher } from "../commands/install.js";
|
|
32
|
+
import { actionScheduler } from "../commands/queue.js";
|
|
33
|
+
import { handleApi, postBodyKind } from "./api.js";
|
|
34
|
+
import { DispatcherControl } from "./dispatcher.js";
|
|
35
|
+
import { TaskEvents } from "./events.js";
|
|
36
|
+
/** Largest POST body read, in bytes. */
|
|
37
|
+
export const MAX_BODY_BYTES = 64 * 1024;
|
|
38
|
+
export const LOOPBACK = "127.0.0.1";
|
|
39
|
+
const SECURITY_HEADERS = {
|
|
40
|
+
"content-security-policy": "default-src 'self'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'",
|
|
41
|
+
"x-content-type-options": "nosniff",
|
|
42
|
+
"referrer-policy": "no-referrer",
|
|
43
|
+
"cache-control": "no-store",
|
|
44
|
+
};
|
|
45
|
+
const JS = "text/javascript; charset=utf-8";
|
|
46
|
+
const CSS = "text/css; charset=utf-8";
|
|
47
|
+
/** ES modules of the page (ui v2). Each is listed: no path touches the disk. */
|
|
48
|
+
export const UI_MODULES = [
|
|
49
|
+
"app.js",
|
|
50
|
+
"api.js",
|
|
51
|
+
"state.js",
|
|
52
|
+
"toast.js",
|
|
53
|
+
"dom.js",
|
|
54
|
+
"views.js",
|
|
55
|
+
"row.js",
|
|
56
|
+
"agenda.js",
|
|
57
|
+
"detail.js",
|
|
58
|
+
"journal.js",
|
|
59
|
+
"timeline.js",
|
|
60
|
+
"dnd.js",
|
|
61
|
+
"keys.js",
|
|
62
|
+
"dialog.js",
|
|
63
|
+
];
|
|
64
|
+
export const STATIC_FILES = {
|
|
65
|
+
"/": { file: "index.html", type: "text/html; charset=utf-8" },
|
|
66
|
+
"/tokens.css": { file: "tokens.css", type: CSS },
|
|
67
|
+
"/app.css": { file: "app.css", type: CSS },
|
|
68
|
+
...Object.fromEntries(UI_MODULES.map((f) => [`/${f}`, { file: f, type: JS }])),
|
|
69
|
+
};
|
|
70
|
+
/** `packages/cli/ui`, from both `src/ui` (tests) and `dist/ui` (published). */
|
|
71
|
+
export function defaultAssetDir() {
|
|
72
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "ui");
|
|
73
|
+
}
|
|
74
|
+
function digest(s) {
|
|
75
|
+
return createHash("sha256").update(s).digest();
|
|
76
|
+
}
|
|
77
|
+
const LOOPBACK_NAMES = new Set([LOOPBACK, "localhost", "[::1]"]);
|
|
78
|
+
/**
|
|
79
|
+
* True when `host` (a Host header, or the host part of an Origin) names a
|
|
80
|
+
* loopback address with no port or with `port`. Anything else, including a
|
|
81
|
+
* DNS name that resolves to 127.0.0.1, is refused.
|
|
82
|
+
*/
|
|
83
|
+
export function isLoopbackHost(host, port) {
|
|
84
|
+
const m = /^(\[::1\]|[^:]+)(?::(\d+))?$/.exec(host.trim().toLowerCase());
|
|
85
|
+
if (!m || !LOOPBACK_NAMES.has(m[1]))
|
|
86
|
+
return false;
|
|
87
|
+
return m[2] === undefined || Number(m[2]) === port;
|
|
88
|
+
}
|
|
89
|
+
function send(res, status, body, type = "application/json; charset=utf-8") {
|
|
90
|
+
res.writeHead(status, { ...SECURITY_HEADERS, ...(body === undefined ? {} : { "content-type": type }) });
|
|
91
|
+
if (body === undefined)
|
|
92
|
+
res.end();
|
|
93
|
+
else
|
|
94
|
+
res.end(typeof body === "string" || Buffer.isBuffer(body) ? body : JSON.stringify(body));
|
|
95
|
+
}
|
|
96
|
+
/** Read a request body, keeping at most `max + 1` bytes; `null` when over `max`. */
|
|
97
|
+
function readBody(req, max) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const chunks = [];
|
|
100
|
+
let size = 0;
|
|
101
|
+
req.on("data", (chunk) => {
|
|
102
|
+
size += chunk.length;
|
|
103
|
+
// Keep draining past the cap so the client can read the 413.
|
|
104
|
+
if (size <= max)
|
|
105
|
+
chunks.push(chunk);
|
|
106
|
+
});
|
|
107
|
+
req.on("end", () => resolve(size > max ? null : Buffer.concat(chunks)));
|
|
108
|
+
req.on("error", reject);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/** KTD9 body rules for one POST route. */
|
|
112
|
+
async function checkBody(req, kind) {
|
|
113
|
+
if (kind === "json") {
|
|
114
|
+
const type = (req.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
|
|
115
|
+
if (type !== "application/json") {
|
|
116
|
+
return { ok: false, status: 415, error: "content-type must be application/json" };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const raw = await readBody(req, MAX_BODY_BYTES);
|
|
120
|
+
if (raw === null)
|
|
121
|
+
return { ok: false, status: 413, error: "body too large" };
|
|
122
|
+
if (kind === "none") {
|
|
123
|
+
return raw.length === 0 ? { ok: true, body: {} } : { ok: false, status: 400, error: "this route takes no body" };
|
|
124
|
+
}
|
|
125
|
+
if (raw.length === 0)
|
|
126
|
+
return { ok: true, body: {} };
|
|
127
|
+
let parsed;
|
|
128
|
+
try {
|
|
129
|
+
parsed = JSON.parse(raw.toString("utf8"));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return { ok: false, status: 400, error: "body is not JSON" };
|
|
133
|
+
}
|
|
134
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
135
|
+
return { ok: false, status: 400, error: "body must be a JSON object" };
|
|
136
|
+
}
|
|
137
|
+
return { ok: true, body: parsed };
|
|
138
|
+
}
|
|
139
|
+
export async function startUiServer(opts) {
|
|
140
|
+
const store = new TaskStore({ dbPath: opts.dbPath });
|
|
141
|
+
const events = new TaskEvents(store, opts.pollMs);
|
|
142
|
+
// Writes go through a Scheduler built WITHOUT adapters (KTD8): it can
|
|
143
|
+
// cancel, move, mark run-now and enqueue a reply, never dispatch.
|
|
144
|
+
const ownScheduler = opts.scheduler === undefined;
|
|
145
|
+
const scheduler = opts.scheduler ??
|
|
146
|
+
actionScheduler({ dbPath: opts.dbPath, feed: opts.feed, region: opts.defaultRegion });
|
|
147
|
+
const dispatcher = new DispatcherControl(store, {
|
|
148
|
+
launcher: opts.launcher ?? resolveLauncher(undefined, []),
|
|
149
|
+
dbPath: opts.dbPath,
|
|
150
|
+
daemonStatus: opts.daemonStatus,
|
|
151
|
+
...(opts.spawn ? { spawn: opts.spawn } : {}),
|
|
152
|
+
...(opts.isAlive ? { isAlive: opts.isAlive } : {}),
|
|
153
|
+
...(opts.logDir ? { logDir: opts.logDir } : {}),
|
|
154
|
+
...(opts.now ? { now: opts.now } : {}),
|
|
155
|
+
});
|
|
156
|
+
const ctx = { ...opts, store, scheduler, dispatcher };
|
|
157
|
+
const assetDir = opts.assetDir ?? defaultAssetDir();
|
|
158
|
+
const tokenDigest = digest(opts.token);
|
|
159
|
+
let port = 0;
|
|
160
|
+
const authorized = (req) => {
|
|
161
|
+
const h = req.headers.authorization ?? "";
|
|
162
|
+
const m = /^Bearer (.+)$/.exec(h);
|
|
163
|
+
return !!m && timingSafeEqual(digest(m[1]), tokenDigest);
|
|
164
|
+
};
|
|
165
|
+
const hostAllowed = (req) => {
|
|
166
|
+
if (!isLoopbackHost(req.headers.host ?? "", port))
|
|
167
|
+
return false;
|
|
168
|
+
const origin = req.headers.origin;
|
|
169
|
+
if (origin === undefined)
|
|
170
|
+
return true;
|
|
171
|
+
const m = /^http:\/\/([^/]+)$/i.exec(origin);
|
|
172
|
+
return !!m && isLoopbackHost(m[1], port);
|
|
173
|
+
};
|
|
174
|
+
const openStream = (req, res) => {
|
|
175
|
+
res.writeHead(200, {
|
|
176
|
+
...SECURITY_HEADERS,
|
|
177
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
178
|
+
connection: "keep-alive",
|
|
179
|
+
});
|
|
180
|
+
res.write(`event: hello\ndata: {}\n\n`);
|
|
181
|
+
const unsubscribe = events.subscribe((ids) => {
|
|
182
|
+
res.write(`event: tasks\ndata: ${JSON.stringify({ ids })}\n\n`);
|
|
183
|
+
});
|
|
184
|
+
const keepAlive = setInterval(() => res.write(`: keep-alive\n\n`), 15_000);
|
|
185
|
+
keepAlive.unref();
|
|
186
|
+
req.on("close", () => {
|
|
187
|
+
clearInterval(keepAlive);
|
|
188
|
+
unsubscribe();
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
const server = createServer((req, res) => {
|
|
192
|
+
void (async () => {
|
|
193
|
+
if (!hostAllowed(req))
|
|
194
|
+
return send(res, 403, { error: "forbidden host" });
|
|
195
|
+
const url = new URL(req.url ?? "/", `http://${LOOPBACK}`);
|
|
196
|
+
const method = req.method ?? "GET";
|
|
197
|
+
if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
|
|
198
|
+
if (!authorized(req))
|
|
199
|
+
return send(res, 401, { error: "unauthorized" });
|
|
200
|
+
if (url.pathname === "/api/events") {
|
|
201
|
+
return method === "GET" ? openStream(req, res) : send(res, 405, { error: "method not allowed" });
|
|
202
|
+
}
|
|
203
|
+
let body = {};
|
|
204
|
+
const kind = method === "POST" ? postBodyKind(url.pathname) : undefined;
|
|
205
|
+
if (kind) {
|
|
206
|
+
const checked = await checkBody(req, kind);
|
|
207
|
+
if (!checked.ok)
|
|
208
|
+
return send(res, checked.status, { error: checked.error });
|
|
209
|
+
body = checked.body;
|
|
210
|
+
}
|
|
211
|
+
const out = await handleApi(ctx, method, url.pathname, url.searchParams, body);
|
|
212
|
+
if (out.changedIds)
|
|
213
|
+
events.emit(out.changedIds);
|
|
214
|
+
return send(res, out.status, out.body);
|
|
215
|
+
}
|
|
216
|
+
const asset = Object.hasOwn(STATIC_FILES, url.pathname) ? STATIC_FILES[url.pathname] : undefined;
|
|
217
|
+
if (!asset)
|
|
218
|
+
return send(res, 404, "not found", "text/plain; charset=utf-8");
|
|
219
|
+
if (method !== "GET" && method !== "HEAD")
|
|
220
|
+
return send(res, 405, "method not allowed", "text/plain; charset=utf-8");
|
|
221
|
+
try {
|
|
222
|
+
return send(res, 200, readFileSync(join(assetDir, asset.file)), asset.type);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return send(res, 404, "not found", "text/plain; charset=utf-8");
|
|
226
|
+
}
|
|
227
|
+
})().catch(() => {
|
|
228
|
+
// No error text in the response: it can carry paths or provider detail.
|
|
229
|
+
if (!res.headersSent)
|
|
230
|
+
send(res, 500, { error: "internal error" });
|
|
231
|
+
else
|
|
232
|
+
res.end();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
await new Promise((resolve, reject) => {
|
|
236
|
+
server.once("error", reject);
|
|
237
|
+
server.listen(opts.port ?? 0, LOOPBACK, () => resolve());
|
|
238
|
+
});
|
|
239
|
+
const addr = server.address();
|
|
240
|
+
port = addr.port;
|
|
241
|
+
events.start();
|
|
242
|
+
return {
|
|
243
|
+
port,
|
|
244
|
+
address: addr.address,
|
|
245
|
+
url: `http://${LOOPBACK}:${port}/`,
|
|
246
|
+
server,
|
|
247
|
+
events,
|
|
248
|
+
close: async () => {
|
|
249
|
+
events.stop();
|
|
250
|
+
server.closeAllConnections();
|
|
251
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
252
|
+
if (ownScheduler)
|
|
253
|
+
scheduler.shutdown();
|
|
254
|
+
store.close();
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/ui/server.ts"],"names":[],"mappings":"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"}
|