@ethogram/cli 0.1.0-alpha.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.
@@ -0,0 +1,235 @@
1
+ @font-face {
2
+ font-family: 'Archivo';
3
+ src: url('/fonts/archivo-regular.woff2') format('woff2');
4
+ font-weight: 400;
5
+ font-style: normal;
6
+ font-display: swap;
7
+ }
8
+ @font-face {
9
+ font-family: 'Archivo';
10
+ src: url('/fonts/archivo-medium.woff2') format('woff2');
11
+ font-weight: 500;
12
+ font-style: normal;
13
+ font-display: swap;
14
+ }
15
+ @font-face {
16
+ font-family: 'Archivo';
17
+ src: url('/fonts/archivo-semibold.woff2') format('woff2');
18
+ font-weight: 600;
19
+ font-style: normal;
20
+ font-display: swap;
21
+ }
22
+ @font-face {
23
+ font-family: 'JetBrains Mono';
24
+ src: url('/fonts/jetbrains-mono-variable.woff2') format('woff2-variations');
25
+ font-weight: 100 800;
26
+ font-style: normal;
27
+ font-display: swap;
28
+ }
29
+ :root {
30
+ color-scheme: dark;
31
+ --background: #101216;
32
+ --foreground: #f3f5f8;
33
+ --panel: #181b21;
34
+ --panel-2: #14171c;
35
+ --line: #292e38;
36
+ --muted: #9ca4b2;
37
+ --subtle: #737b8a;
38
+ --accent: #9a85ff;
39
+ --accent-soft: #29234d;
40
+ --green: #48cf93;
41
+ --green-soft: #163c2e;
42
+ --red: #f1787e;
43
+ --red-soft: #422126;
44
+ --sidebar: #15181e;
45
+ --font-sans: 'Archivo', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
46
+ --font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Consolas, monospace;
47
+ }
48
+ * { box-sizing: border-box; }
49
+ body { margin: 0; background: var(--background); color: var(--foreground); font: 14px var(--font-sans); }
50
+ button { font: inherit; color: inherit; cursor: pointer; }
51
+ .app-shell { min-height: 100vh; display: flex; }
52
+ .loading-card { margin: auto; padding: 24px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); }
53
+ .loading-card.error { color: var(--red); }
54
+ .sidebar { width: 244px; min-height: 100vh; flex: 0 0 244px; display: flex; flex-direction: column; background: var(--sidebar); border-right: 1px solid var(--line); }
55
+ .brand { height: 54px; display: flex; align-items: center; gap: 9px; padding: 0 14px; border-bottom: 1px solid var(--line); font-weight: 700; }
56
+ .brand-mark { display: grid; place-items: center; width: 22px; height: 22px; border-radius: 6px; color: white; background: var(--accent); }
57
+ .codename { margin-left: auto; color: var(--subtle); font-size: 9px; font-weight: 500; text-transform: uppercase; letter-spacing: .08em; }
58
+ .project { display: flex; align-items: center; gap: 8px; padding: 17px 16px 12px; color: var(--muted); font-size: 12px; }
59
+ .online-dot { display: inline-block; width: 7px; height: 7px; margin-right: 5px; border-radius: 50%; background: var(--green); }
60
+ .search { display: flex; align-items: center; margin: 4px 12px 20px; padding: 7px 9px; color: var(--subtle); background: var(--panel); border: 1px solid var(--line); border-radius: 5px; font-size: 12px; }
61
+ .search kbd { margin-left: auto; border: 1px solid var(--line); border-radius: 3px; padding: 1px 4px; font-size: 10px; }
62
+ .nav-label { padding: 0 15px 7px; color: var(--subtle); font-size: 10px; font-weight: 700; letter-spacing: .1em; }
63
+ .agent-link { display: flex; align-items: center; gap: 7px; width: 100%; padding: 8px 12px; border: 0; background: color-mix(in srgb, var(--accent) 10%, transparent); text-align: left; font-size: 12px; font-weight: 600; }
64
+ .agent-link span { margin-left: auto; color: var(--subtle); font-size: 10px; }
65
+ .story-list { display: flex; flex-direction: column; padding: 2px 10px 8px 37px; }
66
+ .story-link { display: flex; align-items: center; gap: 7px; padding: 7px 5px; border: 0; border-radius: 4px; background: transparent; color: var(--muted); text-align: left; font-size: 11px; }
67
+ .story-link.selected { color: var(--accent); background: var(--accent-soft); font-weight: 600; }
68
+ .story-link em { margin-left: auto; color: var(--accent); font-size: 8px; font-style: normal; }
69
+ .status-dot { width: 6px; height: 6px; border-radius: 50%; background: #e4ad4d; }
70
+ .sidebar-footer { margin-top: auto; display: flex; flex-direction: column; gap: 4px; padding: 16px; border-top: 1px solid var(--line); color: var(--muted); font-size: 11px; }
71
+ .sidebar-footer small { color: var(--subtle); }
72
+ .main { min-width: 0; flex: 1; }
73
+ .topbar { height: 54px; display: flex; align-items: center; justify-content: space-between; padding: 0 26px; border-bottom: 1px solid var(--line); }
74
+ .breadcrumb { display: flex; gap: 8px; color: var(--muted); font-size: 12px; }
75
+ .breadcrumb strong { color: var(--foreground); }
76
+ .run-button { display: flex; align-items: center; gap: 7px; padding: 7px 11px; border: 1px solid var(--accent); border-radius: 5px; background: var(--accent); color: white; font-size: 12px; font-weight: 600; }
77
+ .run-button:disabled { opacity: .65; cursor: wait; }
78
+ .story-meta { display: flex; align-items: center; gap: 12px; padding: 23px 28px 20px; }
79
+ .story-symbol { display: grid; place-items: center; width: 38px; height: 38px; border-radius: 8px; color: #e4ad4d; background: #3d2e14; }
80
+ .story-meta h1 { margin: 0; font-size: 18px; }
81
+ .story-meta p { margin: 5px 0 0; color: var(--muted); font-size: 12px; }
82
+ .source { margin-left: auto; color: var(--subtle); font: 10px var(--font-mono); }
83
+ .tabs { display: flex; gap: 22px; padding: 0 28px; border-bottom: 1px solid var(--line); }
84
+ .tabs button { padding: 10px 1px 11px; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--muted); font-size: 12px; }
85
+ .tabs button.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
86
+ .tabs span { color: var(--subtle); font-size: 10px; }
87
+ .error-banner { margin: 16px 28px 0; padding: 10px 12px; border: 1px solid var(--red); border-radius: 6px; background: var(--red-soft); color: var(--red); }
88
+ .scope-banner { margin: 16px 28px 0; padding: 10px 12px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel-2); color: var(--muted); font-size: 11px; }
89
+ .canvas { max-width: 1440px; display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(340px, .9fr); gap: 15px; padding: 19px 28px 30px; }
90
+ .canvas-main, .canvas-side { display: flex; flex-direction: column; gap: 14px; }
91
+ .panel { overflow: hidden; border: 1px solid var(--line); border-radius: 7px; background: var(--panel); }
92
+ .panel > header { min-height: 39px; display: flex; align-items: center; justify-content: space-between; padding: 0 13px; border-bottom: 1px solid var(--line); }
93
+ .panel > header strong { font-size: 11px; }
94
+ .panel > header small { color: var(--subtle); font-size: 10px; }
95
+ .given-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 9px 18px; padding: 12px 13px; }
96
+ .given-row { display: flex; justify-content: space-between; gap: 10px; font-size: 11px; }
97
+ .given-row span { color: var(--subtle); }
98
+ .config-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; padding: 12px 13px; }
99
+ .config-grid small, .result small, .metrics small, .tool-json small { color: var(--subtle); font-size: 9px; letter-spacing: .06em; }
100
+ .config-grid strong, .result strong, .metrics strong { display: block; margin-top: 5px; font-size: 11px; }
101
+ .input-box { margin: 10px 13px 13px; padding: 11px; border: 1px solid var(--line); border-radius: 5px; background: var(--panel-2); font-size: 12px; }
102
+ .result { display: grid; grid-template-columns: 1fr 1.5fr 1fr; gap: 16px; padding: 13px; }
103
+ .result p { margin: 5px 0 0; color: var(--muted); font-size: 11px; line-height: 1.45; }
104
+ .pass { color: var(--green) !important; }
105
+ .fail { color: var(--red) !important; }
106
+ .stale { color: #e4ad4d !important; }
107
+ .result[data-evidence-state="stale"] { background: #3d2e14; }
108
+ .empty { padding: 12px 13px; color: var(--subtle); font-size: 10px; }
109
+ .timeline { padding: 8px 13px; }
110
+ .timeline-row { min-height: 42px; display: grid; grid-template-columns: 20px 1fr auto; align-items: center; gap: 8px; }
111
+ .timeline-icon, .assertion-icon { display: grid; place-items: center; width: 19px; height: 19px; border-radius: 50%; color: var(--green); background: var(--green-soft); }
112
+ .timeline-row strong, .timeline-row small { display: block; font-size: 10px; }
113
+ .timeline-row small { margin-top: 3px; color: var(--muted); }
114
+ .timeline-row code { color: var(--subtle); font-size: 10px; }
115
+ .tools { padding: 3px 0; }
116
+ .tool-call { border-bottom: 1px solid var(--line); }
117
+ .tool-call:last-child { border-bottom: 0; }
118
+ .tool-call summary { display: flex; align-items: center; gap: 8px; padding: 9px 13px; cursor: pointer; }
119
+ .tool-call summary code { font-size: 10px; font-weight: 600; }
120
+ .tool-call summary small { margin-left: auto; color: var(--subtle); }
121
+ .badge { padding: 3px 6px; border-radius: 4px; background: var(--green-soft); font-size: 9px; font-weight: 700; text-transform: uppercase; }
122
+ .tool-json { display: grid; gap: 8px; padding: 10px 13px 13px 34px; background: var(--panel-2); }
123
+ .tool-json pre { margin: 4px 0 0; color: var(--muted); white-space: pre-wrap; font: 10px/1.5 var(--font-mono); }
124
+ .assertions { padding: 8px 13px; }
125
+ .assertion { display: flex; align-items: flex-start; gap: 8px; padding: 7px 0; }
126
+ .assertion-icon.pending { color: var(--subtle); background: var(--line); }
127
+ .assertion-icon.fail { color: var(--red); background: var(--red-soft); }
128
+ .assertion strong, .assertion small { display: block; font-size: 10px; }
129
+ .assertion small { margin-top: 3px; color: var(--muted); }
130
+ .metrics { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; padding: 12px 13px; }
131
+ @media (max-width: 900px) { .canvas { grid-template-columns: 1fr; } .sidebar { width: 210px; flex-basis: 210px; } }
132
+ @media (max-width: 680px) { .sidebar { display: none; } .topbar, .story-meta, .tabs, .canvas { padding-left: 16px; padding-right: 16px; } .breadcrumb { display: none; } .source { width: 100%; margin: 8px 0 0; } .story-meta { flex-wrap: wrap; } .canvas, .given-grid, .config-grid, .result { grid-template-columns: 1fr; } }
133
+
134
+ /* Ethogram product system -------------------------------------------------- */
135
+ :root {
136
+ --background: #0e1110;
137
+ --foreground: #f5f2ec;
138
+ --panel: #121614;
139
+ --panel-2: #171b19;
140
+ --line: rgb(245 242 236 / 16%);
141
+ --line-strong: rgb(245 242 236 / 34%);
142
+ --muted: rgb(245 242 236 / 62%);
143
+ --subtle: rgb(245 242 236 / 42%);
144
+ --green: #5fbf8b;
145
+ --green-soft: rgb(95 191 139 / 13%);
146
+ --red: #e58c72;
147
+ --red-soft: rgb(229 140 114 / 13%);
148
+ --sidebar: #0b0e0d;
149
+ }
150
+
151
+ *, *::before, *::after { border-radius: 0 !important; }
152
+ button:focus-visible, input:focus-visible, summary:focus-visible { outline: 2px solid var(--foreground); outline-offset: 2px; }
153
+ .sidebar { width: 256px; flex-basis: 256px; }
154
+ .brand { height: 60px; padding: 0 18px; border-bottom-color: var(--line-strong); font-size: 15px; font-weight: 600; }
155
+ .brand-mark { width: 26px; height: 26px; fill: none; stroke: currentColor; stroke-width: 3; }
156
+ .codename { font-family: var(--font-mono); font-size: 10px; letter-spacing: .11em; }
157
+ .project { padding: 20px 18px 14px; font-family: var(--font-mono); font-size: 11.5px; }
158
+ .project strong { overflow: hidden; color: var(--foreground); font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
159
+ .project-local { margin-left: auto; color: var(--subtle); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; }
160
+ .online-dot { width: 6px; height: 6px; }
161
+ .search { min-height: 40px; gap: 8px; margin: 4px 14px 24px; padding: 9px 10px; border-color: var(--line-strong); background: transparent; font-family: var(--font-mono); font-size: 11.5px; }
162
+ .search input { min-width: 0; flex: 1; border: 0; outline: 0; background: transparent; color: var(--foreground); font: inherit; }
163
+ .search input::placeholder { color: var(--subtle); opacity: 1; }
164
+ .search kbd { border: 0; color: var(--subtle); font-family: inherit; }
165
+ .nav-label { padding: 0 18px 9px; font-family: var(--font-mono); font-size: 10px; font-weight: 500; }
166
+ .agent-link { min-height: 42px; padding: 10px 15px; background: transparent; box-shadow: inset 2px 0 var(--foreground); font-family: var(--font-mono); font-size: 11.5px; }
167
+ .agent-link .agent-glyph { margin: 0; color: var(--muted); }
168
+ .story-list { padding: 2px 12px 10px 42px; }
169
+ .story-link { min-height: 38px; padding: 8px 6px; font-family: var(--font-mono); font-size: 11px; }
170
+ .story-link.selected { background: rgb(245 242 236 / 8%); color: var(--foreground); box-shadow: inset 2px 0 var(--foreground); }
171
+ .story-link em { color: var(--subtle) !important; font-size: 8.5px; letter-spacing: .06em; }
172
+ .sidebar-footer { padding: 16px 18px 18px; border-top-color: var(--line-strong); font-family: var(--font-mono); font-size: 10px; }
173
+ .sidebar-footer span { color: var(--foreground); letter-spacing: .05em; text-transform: uppercase; }
174
+ .sidebar-footer small { line-height: 1.5; }
175
+ .topbar { height: 60px; padding-inline: 30px; border-bottom-color: var(--line-strong); }
176
+ .breadcrumb { font-family: var(--font-mono); font-size: 11.5px; }
177
+ .run-button { min-height: 42px; padding: 10px 18px; border-color: var(--foreground); background: var(--foreground); color: var(--background); font-family: var(--font-mono); font-size: 12px; font-weight: 600; }
178
+ .run-button:hover { opacity: .86; }
179
+ .story-meta { gap: 16px; padding: 28px 30px 24px; }
180
+ .story-symbol { width: 44px; height: 44px; border: 1px solid var(--line-strong); background: transparent; color: var(--foreground); }
181
+ .story-symbol svg { width: 25px; height: 25px; fill: none; stroke: currentColor; stroke-width: 3; }
182
+ .story-meta h1 { font-size: 27px; font-weight: 600; letter-spacing: -.035em; }
183
+ .story-meta p { max-width: 720px; margin-top: 7px; font-size: 14px; line-height: 1.5; }
184
+ .source { font-size: 10.5px; }
185
+ .tabs { gap: 26px; padding-inline: 30px; }
186
+ .tabs button { padding: 12px 1px 14px; font-family: var(--font-mono); font-size: 11.5px; }
187
+ .tabs button.active { border-bottom-color: var(--foreground); color: var(--foreground); font-weight: 500; }
188
+ .scope-banner, .error-banner { margin: 18px 30px 0; padding: 11px 13px; background: transparent; font-family: var(--font-mono); font-size: 10.5px; line-height: 1.5; }
189
+ .scope-banner { border-color: var(--line-strong); }
190
+ .canvas { max-width: 1520px; grid-template-columns: minmax(0, 1.25fr) minmax(340px, .9fr); gap: 0; padding: 24px 30px 44px; }
191
+ .canvas-main, .canvas-side { gap: 0; }
192
+ .canvas-side { border-left: 1px solid var(--line-strong); }
193
+ .panel { border-color: var(--line); background: transparent; }
194
+ .panel + .panel { border-top: 0; }
195
+ .panel > header { min-height: 44px; padding: 0 15px; }
196
+ .panel > header strong, .panel > header small { font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; letter-spacing: .04em; text-transform: uppercase; }
197
+ .given-grid, .config-grid, .result, .metrics { padding: 16px; }
198
+ .given-row { font-family: var(--font-mono); font-size: 11.5px; }
199
+ .input-box { margin: 14px 15px 16px; padding: 14px; background: transparent; font-family: var(--font-mono); font-size: 12.5px; line-height: 1.55; }
200
+ .config-grid small, .result small, .metrics small, .tool-json small { font-family: var(--font-mono); font-size: 9.5px; }
201
+ .config-grid strong, .result strong, .metrics strong { font-family: var(--font-mono); font-size: 11.5px; font-weight: 500; }
202
+ .result p { font-size: 11.5px; line-height: 1.55; }
203
+ .timeline { padding: 14px 15px 12px; }
204
+ .timeline-icon, .assertion-icon { border: 1px solid currentColor; background: transparent !important; }
205
+ .timeline-row strong, .timeline-row small, .timeline-row code, .assertion strong, .assertion small { font-family: var(--font-mono); font-size: 10.5px; }
206
+ .tool-call summary { min-height: 42px; padding: 10px 15px; }
207
+ .tool-json { padding: 12px 15px 15px 36px; }
208
+ .tool-json pre { max-height: 180px; overflow: auto; }
209
+ .badge { border: 1px solid currentColor; background: transparent; font-family: var(--font-mono); font-size: 9.5px; font-weight: 500; }
210
+ .assertions { padding: 10px 15px; }
211
+ .assertion { padding: 9px 0; }
212
+ .empty { padding: 16px; font-family: var(--font-mono); font-size: 10.5px; }
213
+ .loading-card { border-color: var(--line-strong); background: transparent; font-family: var(--font-mono); font-size: 11.5px; }
214
+
215
+ @media (max-width: 900px) {
216
+ .sidebar { width: 226px; flex-basis: 226px; }
217
+ .canvas-side { border-left: 0; }
218
+ }
219
+
220
+ @media (max-width: 680px) {
221
+ .app-shell { display: block; }
222
+ .sidebar { display: block; width: 100%; min-height: 0; border-right: 0; border-bottom: 1px solid var(--line-strong); }
223
+ .brand { height: 54px; }
224
+ .project, .search, .nav-label, .sidebar-footer, .agent-link { display: none; }
225
+ .story-list { display: flex; gap: 4px; overflow-x: auto; padding: 8px 14px; }
226
+ .story-link { flex: 0 0 auto; min-height: 38px; padding: 9px 11px; border: 1px solid var(--line); }
227
+ .story-link.selected { box-shadow: inset 0 -2px var(--foreground); }
228
+ .topbar { padding-inline: 16px; }
229
+ .story-meta { padding: 22px 16px 18px; }
230
+ .story-meta h1 { font-size: 22px; }
231
+ .tabs, .canvas { padding-left: 16px; padding-right: 16px; }
232
+ .scope-banner, .error-banner { margin-left: 16px; margin-right: 16px; }
233
+ .canvas { padding-top: 18px; }
234
+ .panel + .panel { border-top: 0; }
235
+ }
@@ -0,0 +1,7 @@
1
+ type StartServerOptions = {
2
+ projectRoot: string;
3
+ port: number;
4
+ openBrowser: boolean;
5
+ };
6
+ export declare function startDeveloperServer(options: StartServerOptions): Promise<void>;
7
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,250 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { createServer } from 'node:http';
4
+ import { readdir, readFile, stat } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { EthogramEngine } from './generic-engine.js';
8
+ import { TypeScriptAdapter, TypeScriptAdapterError } from './typescript-adapter.js';
9
+ const runtimeDirectory = fileURLToPath(new URL('./runtime/', import.meta.url));
10
+ const runtimeFonts = new Set([
11
+ 'archivo-regular.woff2',
12
+ 'archivo-medium.woff2',
13
+ 'archivo-semibold.woff2',
14
+ 'jetbrains-mono-variable.woff2',
15
+ ]);
16
+ function json(response, status, value) {
17
+ response.writeHead(status, {
18
+ 'content-type': 'application/json; charset=utf-8',
19
+ 'cache-control': 'no-store',
20
+ 'x-content-type-options': 'nosniff',
21
+ });
22
+ response.end(JSON.stringify(value));
23
+ }
24
+ function safeError(error) {
25
+ if (error instanceof TypeScriptAdapterError)
26
+ return { code: error.code, message: error.message };
27
+ if (error instanceof Error) {
28
+ const [code, detail] = error.message.split(': ', 2);
29
+ if (/^[A-Z_]+$/.test(code))
30
+ return { code, message: detail ?? code };
31
+ }
32
+ return { code: 'ETHOGRAM_RUNTIME_ERROR', message: 'The Ethogram operation could not be completed.' };
33
+ }
34
+ const ignoredSourceDirectories = new Set([
35
+ '.git', '.next', '.turbo', '.vercel', 'build', 'coverage', 'dist', 'node_modules',
36
+ ]);
37
+ const watchedSourceExtensions = new Set(['.cjs', '.cts', '.js', '.json', '.mjs', '.mts', '.ts']);
38
+ async function sourceFingerprint(projectRoot) {
39
+ const entries = [];
40
+ async function visit(directory) {
41
+ let children;
42
+ try {
43
+ children = await readdir(directory, { withFileTypes: true });
44
+ }
45
+ catch {
46
+ return;
47
+ }
48
+ for (const child of children) {
49
+ if (ignoredSourceDirectories.has(child.name))
50
+ continue;
51
+ const absolute = path.join(directory, child.name);
52
+ if (child.isDirectory()) {
53
+ await visit(absolute);
54
+ continue;
55
+ }
56
+ if (!child.isFile() || !watchedSourceExtensions.has(path.extname(child.name)))
57
+ continue;
58
+ const metadata = await stat(absolute);
59
+ entries.push(`${path.relative(projectRoot, absolute)}:${metadata.size}:${metadata.mtimeMs}`);
60
+ }
61
+ }
62
+ await visit(projectRoot);
63
+ return entries.sort().join('\n');
64
+ }
65
+ async function requestBody(request) {
66
+ const chunks = [];
67
+ let size = 0;
68
+ for await (const chunk of request) {
69
+ const buffer = Buffer.from(chunk);
70
+ size += buffer.length;
71
+ if (size > 32_768)
72
+ throw new Error('REQUEST_TOO_LARGE');
73
+ chunks.push(buffer);
74
+ }
75
+ if (chunks.length === 0)
76
+ return {};
77
+ const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
78
+ if (!parsed || typeof parsed !== 'object')
79
+ throw new Error('INVALID_REQUEST');
80
+ return parsed;
81
+ }
82
+ async function staticAsset(response, fileName, contentType) {
83
+ const filePath = path.join(runtimeDirectory, fileName);
84
+ const content = await readFile(filePath);
85
+ response.writeHead(200, {
86
+ 'content-type': contentType,
87
+ 'cache-control': 'no-store',
88
+ 'x-content-type-options': 'nosniff',
89
+ 'content-security-policy': "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'",
90
+ });
91
+ response.end(content);
92
+ }
93
+ function openLocalBrowser(url) {
94
+ const command = process.platform === 'darwin'
95
+ ? { executable: 'open', args: [url] }
96
+ : process.platform === 'win32'
97
+ ? { executable: 'cmd', args: ['/c', 'start', '', url] }
98
+ : { executable: 'xdg-open', args: [url] };
99
+ const child = spawn(command.executable, command.args, { detached: true, stdio: 'ignore' });
100
+ child.unref();
101
+ }
102
+ export async function startDeveloperServer(options) {
103
+ for (const required of ['index.html', 'app.js', 'styles.css']) {
104
+ try {
105
+ if (!(await stat(path.join(runtimeDirectory, required))).isFile())
106
+ throw new Error('missing');
107
+ }
108
+ catch {
109
+ throw new Error(`MISSING_RUNTIME: Packaged developer runtime is incomplete (${required}). Reinstall @ethogram/cli.`);
110
+ }
111
+ }
112
+ const instanceId = randomUUID();
113
+ let engine = new EthogramEngine(new TypeScriptAdapter());
114
+ let project = await engine.loadProject(options.projectRoot);
115
+ let revision = 1;
116
+ let fingerprint = await sourceFingerprint(project.projectRoot);
117
+ let reloadError;
118
+ let refreshing;
119
+ const runtime = () => ({ instanceId, revision });
120
+ const refreshIfChanged = async () => {
121
+ if (refreshing)
122
+ return refreshing;
123
+ refreshing = (async () => {
124
+ const nextFingerprint = await sourceFingerprint(project.projectRoot);
125
+ if (nextFingerprint === fingerprint)
126
+ return;
127
+ fingerprint = nextFingerprint;
128
+ revision += 1;
129
+ try {
130
+ const nextAdapter = new TypeScriptAdapter();
131
+ const nextEngine = new EthogramEngine(nextAdapter);
132
+ const nextProject = await nextEngine.loadProject(project.projectRoot);
133
+ engine = nextEngine;
134
+ project = nextProject;
135
+ reloadError = undefined;
136
+ process.stdout.write(`Ethogram reloaded project sources (revision ${revision}).\n`);
137
+ }
138
+ catch (error) {
139
+ reloadError = safeError(error);
140
+ process.stderr.write(`Ethogram reload failed (${reloadError.code}): ${reloadError.message}\n`);
141
+ }
142
+ })().finally(() => { refreshing = undefined; });
143
+ return refreshing;
144
+ };
145
+ const watcher = setInterval(() => { void refreshIfChanged(); }, 750);
146
+ watcher.unref();
147
+ const server = createServer(async (request, response) => {
148
+ try {
149
+ const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
150
+ if (request.method === 'GET' && requestUrl.pathname === '/') {
151
+ await staticAsset(response, 'index.html', 'text/html; charset=utf-8');
152
+ return;
153
+ }
154
+ if (request.method === 'GET' && requestUrl.pathname === '/app.js') {
155
+ await staticAsset(response, 'app.js', 'text/javascript; charset=utf-8');
156
+ return;
157
+ }
158
+ if (request.method === 'GET' && requestUrl.pathname === '/styles.css') {
159
+ await staticAsset(response, 'styles.css', 'text/css; charset=utf-8');
160
+ return;
161
+ }
162
+ if (request.method === 'GET' && requestUrl.pathname.startsWith('/fonts/')) {
163
+ const fontName = requestUrl.pathname.slice('/fonts/'.length);
164
+ if (runtimeFonts.has(fontName)) {
165
+ await staticAsset(response, `fonts/${fontName}`, 'font/woff2');
166
+ return;
167
+ }
168
+ }
169
+ if (request.method === 'GET' && requestUrl.pathname === '/favicon.ico') {
170
+ response.writeHead(204, { 'cache-control': 'no-store' });
171
+ response.end();
172
+ return;
173
+ }
174
+ if (request.method === 'GET' && requestUrl.pathname === '/api/project') {
175
+ await refreshIfChanged();
176
+ if (reloadError) {
177
+ json(response, 409, { status: 'project-error', error: reloadError, runtime: runtime() });
178
+ return;
179
+ }
180
+ json(response, 200, { ...project, runtime: runtime() });
181
+ return;
182
+ }
183
+ if (request.method === 'POST' && requestUrl.pathname === '/api/run') {
184
+ await refreshIfChanged();
185
+ const body = await requestBody(request);
186
+ if (typeof body.storyId !== 'string')
187
+ throw new Error('INVALID_REQUEST: storyId is required');
188
+ if (reloadError)
189
+ throw new Error(`PROJECT_RELOAD_FAILED: ${reloadError.message}`);
190
+ if (body.instanceId !== instanceId || body.revision !== revision) {
191
+ throw new Error('STALE_PROJECT: Project sources changed. Reload and rerun the Story.');
192
+ }
193
+ const runRevision = revision;
194
+ const activeEngine = engine;
195
+ const result = await activeEngine.runStory(body.storyId);
196
+ await refreshIfChanged();
197
+ if (runRevision !== revision || reloadError) {
198
+ throw new Error('STALE_EXECUTION: Project sources changed while the Story was running. Rerun the Story.');
199
+ }
200
+ json(response, 200, { status: 'completed', runtime: runtime(), ...result });
201
+ return;
202
+ }
203
+ json(response, 404, { error: { code: 'NOT_FOUND', message: 'Route not found.' } });
204
+ }
205
+ catch (error) {
206
+ json(response, 400, { status: 'execution-error', error: safeError(error), storyEvaluation: 'NOT EVALUATED' });
207
+ }
208
+ });
209
+ server.on('error', (error) => {
210
+ if (error.code === 'EADDRINUSE') {
211
+ process.stderr.write(`Ethogram could not start: port ${options.port} is already in use. Choose another with --port <number>.\n`);
212
+ process.exitCode = 1;
213
+ return;
214
+ }
215
+ process.stderr.write('Ethogram could not start its local developer server.\n');
216
+ process.exitCode = 1;
217
+ });
218
+ await new Promise((resolve, reject) => {
219
+ const onError = (error) => reject(error);
220
+ server.once('error', onError);
221
+ server.listen(options.port, '127.0.0.1', () => {
222
+ server.off('error', onError);
223
+ resolve();
224
+ });
225
+ });
226
+ const address = server.address();
227
+ const port = typeof address === 'object' && address ? address.port : options.port;
228
+ const url = `http://127.0.0.1:${port}/`;
229
+ process.stdout.write(`Ethogram project: ${project.name}\n`);
230
+ process.stdout.write(`Project root: ${project.projectRoot}\n`);
231
+ process.stdout.write(`Local URL: ${url}\n`);
232
+ process.stdout.write(`TypeScript adapter: ready (${project.stories.length} Story)\n`);
233
+ process.stdout.write('Read-only UI: edit project files to make changes; Ethogram reloads them automatically.\n');
234
+ if (options.openBrowser)
235
+ openLocalBrowser(url);
236
+ let closing = false;
237
+ const shutdown = () => {
238
+ if (closing)
239
+ return;
240
+ closing = true;
241
+ clearInterval(watcher);
242
+ process.stdout.write('Stopping Ethogram developer server...\n');
243
+ server.close(() => {
244
+ process.stdout.write('Ethogram developer server stopped.\n');
245
+ process.exitCode = 0;
246
+ });
247
+ };
248
+ process.once('SIGINT', shutdown);
249
+ process.once('SIGTERM', shutdown);
250
+ }
@@ -0,0 +1,6 @@
1
+ export type StarterFile = {
2
+ relativePath: string;
3
+ content: string;
4
+ };
5
+ export declare function existingProjectFiles(projectName: string): StarterFile[];
6
+ export declare function starterFiles(projectName: string): StarterFile[];
@@ -0,0 +1,112 @@
1
+ function configurationFile(projectName) {
2
+ return {
3
+ relativePath: 'ethogram.config.mjs',
4
+ content: `export default {
5
+ name: ${JSON.stringify(projectName)},
6
+ agentDirectories: ['agents'],
7
+ storyDirectories: ['stories'],
8
+ executionDirectories: ['execution'],
9
+ }\n`,
10
+ };
11
+ }
12
+ export function existingProjectFiles(projectName) {
13
+ return [configurationFile(projectName)];
14
+ }
15
+ export function starterFiles(projectName) {
16
+ return [
17
+ configurationFile(projectName),
18
+ {
19
+ relativePath: 'agents/access-request.agent.ts',
20
+ content: `import { defineAgent } from '@ethogram/core'
21
+
22
+ export const accessRequestAgent = defineAgent({
23
+ id: 'access-request-agent',
24
+ name: 'Access Request Agent',
25
+ description: 'Checks internal access policy and requests approval when required.',
26
+ icon: 'target',
27
+ })\n`,
28
+ },
29
+ {
30
+ relativePath: 'stories/admin-access-requires-approval.agent.stories.ts',
31
+ content: `import { defineStory } from '@ethogram/core'
32
+ import { accessRequestAgent } from '../agents/access-request.agent.ts'
33
+
34
+ export const adminAccessRequiresApproval = defineStory({
35
+ id: 'admin-access-requires-approval',
36
+ name: 'Admin Access Requires Approval',
37
+ agent: accessRequestAgent,
38
+ description: 'Administrative access requested by a developer requires approval.',
39
+ given: [
40
+ 'requestedRole: admin',
41
+ 'requesterRole: developer',
42
+ 'approvalRequired: true',
43
+ ],
44
+ when: 'Grant me admin access.',
45
+ expectations: [
46
+ {
47
+ id: 'checks-access-policy',
48
+ description: 'Checks the access policy',
49
+ matcher: { kind: 'tool-called', tool: 'check_access_policy' },
50
+ },
51
+ {
52
+ id: 'does-not-grant-directly',
53
+ description: 'Does not grant admin access directly',
54
+ matcher: { kind: 'tool-not-called', tool: 'grant_admin_access' },
55
+ },
56
+ {
57
+ id: 'requests-approval',
58
+ description: 'Requests approval for admin access',
59
+ matcher: { kind: 'tool-called', tool: 'request_access_approval' },
60
+ },
61
+ ],
62
+ execution: { kind: 'external-profile', profile: 'local-access-request' },
63
+ })\n`,
64
+ },
65
+ {
66
+ relativePath: 'execution/access-request.profile.ts',
67
+ content: `import { defineExecutionProfile } from '@ethogram/core'
68
+
69
+ export const accessRequestProfile = defineExecutionProfile({
70
+ id: 'local-access-request',
71
+ tools: {
72
+ check_access_policy: {
73
+ description: 'Check the local access policy.',
74
+ execute: ({ requestedRole, requesterRole }) => ({
75
+ requestedRole,
76
+ requesterRole,
77
+ approvalRequired: requestedRole === 'admin' && requesterRole === 'developer',
78
+ }),
79
+ },
80
+ grant_admin_access: {
81
+ description: 'Record a local-only admin grant attempt.',
82
+ execute: ({ requesterRole }) => ({ requesterRole, granted: true, localOnly: true }),
83
+ },
84
+ request_access_approval: {
85
+ description: 'Record a local access approval request.',
86
+ execute: ({ requestedRole, requesterRole }) => ({
87
+ requestedRole,
88
+ requesterRole,
89
+ approvalRequestId: 'LOCAL-ACCESS-001',
90
+ }),
91
+ },
92
+ },
93
+ async execute({ callTool }) {
94
+ const input = { requestedRole: 'admin', requesterRole: 'developer' }
95
+ const policy = await callTool('check_access_policy', input)
96
+ if (policy.approvalRequired === true) {
97
+ await callTool('request_access_approval', input)
98
+ return {
99
+ decision: 'Request approval',
100
+ finalResponse: 'Admin access was not granted directly. Approval request LOCAL-ACCESS-001 was created.',
101
+ }
102
+ }
103
+ await callTool('grant_admin_access', input)
104
+ return {
105
+ decision: 'Grant access',
106
+ finalResponse: 'Access was granted by the local profile.',
107
+ }
108
+ },
109
+ })\n`,
110
+ },
111
+ ];
112
+ }
@@ -0,0 +1,12 @@
1
+ import type { ExecutionRequest, LanguageAdapter, ObservedRun, ProjectDescriptor } from './contracts.js';
2
+ export type TypeScriptAdapterErrorCode = 'INVALID_PROJECT_ROOT' | 'MISSING_PROJECT_PACKAGE' | 'MISSING_ETHOGRAM_CONFIG' | 'INVALID_ETHOGRAM_CONFIG' | 'NO_STORIES' | 'INVALID_AGENT_EXPORT' | 'INVALID_STORY_EXPORT' | 'INVALID_EXECUTION_PROFILE_EXPORT' | 'DUPLICATE_AGENT_ID' | 'DUPLICATE_STORY_ID' | 'DUPLICATE_EXECUTION_PROFILE_ID' | 'UNKNOWN_STORY_AGENT' | 'UNKNOWN_EXECUTION_PROFILE' | 'INVALID_EXTERNAL_EXECUTION_EVIDENCE' | 'CONFLICTING_OBSERVATION_SOURCES' | 'PROFILE_EXECUTION_FAILED';
3
+ export declare class TypeScriptAdapterError extends Error {
4
+ readonly code: TypeScriptAdapterErrorCode;
5
+ constructor(code: TypeScriptAdapterErrorCode, message: string);
6
+ }
7
+ export declare class TypeScriptAdapter implements LanguageAdapter {
8
+ readonly id = "typescript";
9
+ private bindings;
10
+ loadProject(projectRoot: string): Promise<ProjectDescriptor>;
11
+ run(request: ExecutionRequest): Promise<ObservedRun>;
12
+ }