eklavya 1.25.3 → 1.27.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/dist/artifacts.js +214 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/assets/artifact-template.html +144 -0
- package/dist/assets/dashboard.html +119 -2
- package/dist/assets/tutor/SKILL.md +7 -0
- package/dist/cli-memory.js +6 -3
- package/dist/cli-memory.js.map +1 -1
- package/dist/cli.js +185 -5
- package/dist/cli.js.map +1 -1
- package/dist/config.js +10 -2
- package/dist/config.js.map +1 -1
- package/dist/dashboard.js +41 -1
- package/dist/dashboard.js.map +1 -1
- package/dist/hooks/memory-lib.js +1 -1
- package/dist/hooks/memory-lib.js.map +1 -1
- package/dist/hooks/session-start.js +34 -3
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/install-lock.js +155 -0
- package/dist/install-lock.js.map +1 -0
- package/dist/install.js +82 -60
- package/dist/install.js.map +1 -1
- package/dist/memory/reservation.js +15 -5
- package/dist/memory/reservation.js.map +1 -1
- package/dist/memory/worker.js +72 -31
- package/dist/memory/worker.js.map +1 -1
- package/dist/migrations/015_event_link_indexes.sql +24 -0
- package/dist/onboard.js +29 -15
- package/dist/onboard.js.map +1 -1
- package/dist/paths.js +8 -0
- package/dist/paths.js.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/explainer.md +55 -0
- package/dist/plugin/agents/tutor.md +7 -0
- package/dist/plugin/hooks/CLAUDE.md +15 -3
- package/dist/plugin/hooks/run.mjs +104 -21
- package/dist/plugin/skills/CLAUDE.md +7 -0
- package/dist/plugin/skills/tutor/SKILL.md +7 -0
- package/dist/tools/config_tools.js +8 -0
- package/dist/tools/config_tools.js.map +1 -1
- package/dist/tools/record_attempt.js +25 -1
- package/dist/tools/record_attempt.js.map +1 -1
- package/dist/update.js +284 -0
- package/dist/update.js.map +1 -0
- package/dist/user-skill/eklavya/SKILL.md +18 -1
- package/dist/user-skill/eklavya-artifacts/SKILL.md +116 -0
- package/package.json +1 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifacts: self-contained HTML pages an agent writes to explain something —
|
|
3
|
+
* a concept the developer got wrong, a design, a report — kept under
|
|
4
|
+
* `~/.eklavya/artifacts/<project slug>/`.
|
|
5
|
+
*
|
|
6
|
+
* Three rules shape this file.
|
|
7
|
+
*
|
|
8
|
+
* **The files are the index.** There is no table. `listArtifacts` reads each
|
|
9
|
+
* page's `<head>` for the metadata `createArtifact` stamped into it, so a page
|
|
10
|
+
* deleted, renamed or copied in by hand is reflected on the next read, and
|
|
11
|
+
* there is nothing to fall out of step with the disk. The ceiling is a scan of
|
|
12
|
+
* every file per dashboard load; `HEAD_BYTES` keeps each read small, and a few
|
|
13
|
+
* thousand pages is still milliseconds.
|
|
14
|
+
*
|
|
15
|
+
* **The design tokens are inlined at creation, not copied into the template.**
|
|
16
|
+
* `tokens.css` is the one palette the site and the dashboard share; a second
|
|
17
|
+
* hand-kept copy inside a template is the copy nobody updates. Inlining makes
|
|
18
|
+
* the page self-contained — its "download HTML" button hands over a file that
|
|
19
|
+
* still themes and prints — without a second source.
|
|
20
|
+
*
|
|
21
|
+
* **Every path in is untrusted.** `resolveArtifact` is what the dashboard
|
|
22
|
+
* serves through, and it refuses anything that does not land on an `.html`
|
|
23
|
+
* file directly inside one project folder.
|
|
24
|
+
*/
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { findRepoConfig, mainRepoRoot } from './config.js';
|
|
29
|
+
import { artifactsDir, projectSlug } from './paths.js';
|
|
30
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
/** Enough of a file to hold its `<head>`: the metadata is written first. */
|
|
32
|
+
const HEAD_BYTES = 16 * 1024;
|
|
33
|
+
/** What a page is filed under: the main checkout behind a worktree, else the directory itself. */
|
|
34
|
+
export function artifactProject(cwd = process.cwd()) {
|
|
35
|
+
const { repoRoot } = findRepoConfig(cwd);
|
|
36
|
+
if (repoRoot)
|
|
37
|
+
return mainRepoRoot(repoRoot);
|
|
38
|
+
try {
|
|
39
|
+
return fs.realpathSync(path.resolve(cwd));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return path.resolve(cwd);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function kebab(title) {
|
|
46
|
+
const s = title
|
|
47
|
+
.normalize('NFKD')
|
|
48
|
+
.replace(/[̀-ͯ]/g, '')
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
51
|
+
.replace(/^-+|-+$/g, '')
|
|
52
|
+
.slice(0, 60)
|
|
53
|
+
.replace(/-+$/, '');
|
|
54
|
+
return s || 'artifact';
|
|
55
|
+
}
|
|
56
|
+
const escHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
57
|
+
function assetPath(name) {
|
|
58
|
+
return path.join(moduleDir, 'assets', name);
|
|
59
|
+
}
|
|
60
|
+
/** The shared tokens, minus the remote font import (the template links the fonts itself). */
|
|
61
|
+
function inlineTokens() {
|
|
62
|
+
try {
|
|
63
|
+
const css = fs.readFileSync(assetPath('tokens.css'), 'utf8');
|
|
64
|
+
return css.replace(/@import\s+url\([^)]*\)[^;]*;\s*/gi, '');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// A build without the tokens still yields a readable page: the template's
|
|
68
|
+
// own fallbacks cover the roles it names.
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Writes the template, filled in, to a fresh file and returns its path. Never
|
|
74
|
+
* overwrites: a second page with the same title on the same day gets `-2`.
|
|
75
|
+
*/
|
|
76
|
+
export function createArtifact(opts) {
|
|
77
|
+
const title = opts.title.trim();
|
|
78
|
+
if (!title)
|
|
79
|
+
throw new Error('an artifact needs a title');
|
|
80
|
+
const now = opts.now ?? new Date();
|
|
81
|
+
const project = artifactProject(opts.cwd);
|
|
82
|
+
const folder = projectSlug(project);
|
|
83
|
+
const dir = path.join(artifactsDir(), folder);
|
|
84
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
85
|
+
// The local date, not UTC: the filename is for the person reading their folder.
|
|
86
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
87
|
+
const day = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
88
|
+
const stem = `${day}-${kebab(title)}`;
|
|
89
|
+
let file = `${stem}.html`;
|
|
90
|
+
for (let n = 2; fs.existsSync(path.join(dir, file)); n++)
|
|
91
|
+
file = `${stem}-${n}.html`;
|
|
92
|
+
const description = (opts.description ?? '').trim();
|
|
93
|
+
const kind = opts.kind ?? 'artifact';
|
|
94
|
+
const fill = {
|
|
95
|
+
'{{TITLE}}': escHtml(title),
|
|
96
|
+
'{{DESCRIPTION}}': escHtml(description),
|
|
97
|
+
'{{PROJECT}}': escHtml(project),
|
|
98
|
+
'{{PROJECT_NAME}}': escHtml(path.basename(project) || project),
|
|
99
|
+
'{{KIND}}': kind,
|
|
100
|
+
'{{CONCEPT}}': escHtml(opts.concept ?? ''),
|
|
101
|
+
'{{CREATED}}': now.toISOString(),
|
|
102
|
+
'{{EYEBROW}}': kind === 'explainer' ? 'Explainer' : 'Artifact',
|
|
103
|
+
};
|
|
104
|
+
let html = fs.readFileSync(assetPath('artifact-template.html'), 'utf8');
|
|
105
|
+
// Tokens first: the CSS contains no placeholders, but user text could.
|
|
106
|
+
html = html.replace('/*{{TOKENS}}*/', () => inlineTokens());
|
|
107
|
+
html = html.replace(/\{\{[A-Z_]+\}\}/g, (m) => fill[m] ?? m);
|
|
108
|
+
const target = path.join(dir, file);
|
|
109
|
+
fs.writeFileSync(target, html, { flag: 'wx' });
|
|
110
|
+
return { path: target, id: `${folder}/${file}`, project };
|
|
111
|
+
}
|
|
112
|
+
function meta(head, name) {
|
|
113
|
+
const re = new RegExp(`<meta\\s+name="${name}"\\s+content="([^"]*)"`, 'i');
|
|
114
|
+
const m = re.exec(head);
|
|
115
|
+
return m ? unescHtml(m[1]) : null;
|
|
116
|
+
}
|
|
117
|
+
function unescHtml(s) {
|
|
118
|
+
return s.replace(/&(amp|lt|gt|quot|#39);/g, (_, e) => ({ amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'" })[e]);
|
|
119
|
+
}
|
|
120
|
+
function readHead(file) {
|
|
121
|
+
const fd = fs.openSync(file, 'r');
|
|
122
|
+
try {
|
|
123
|
+
const buf = Buffer.alloc(HEAD_BYTES);
|
|
124
|
+
const n = fs.readSync(fd, buf, 0, HEAD_BYTES, 0);
|
|
125
|
+
return buf.subarray(0, n).toString('utf8');
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
fs.closeSync(fd);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Every artifact on disk, newest first. A page with no Eklavya metadata — one
|
|
133
|
+
* copied in by hand — is still listed, titled from its `<title>` or filename.
|
|
134
|
+
* Never throws: an unreadable file costs that one row.
|
|
135
|
+
*/
|
|
136
|
+
export function listArtifacts(root = artifactsDir()) {
|
|
137
|
+
const rows = [];
|
|
138
|
+
let folders;
|
|
139
|
+
try {
|
|
140
|
+
folders = fs.readdirSync(root, { withFileTypes: true });
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return rows;
|
|
144
|
+
}
|
|
145
|
+
for (const folder of folders) {
|
|
146
|
+
// The same rules `resolveArtifact` serves by, so nothing is listed that
|
|
147
|
+
// the dashboard would then refuse: no dot-names, no symlinks — a link's
|
|
148
|
+
// `<head>` would otherwise be read from wherever it points.
|
|
149
|
+
if (!folder.isDirectory() || folder.name.startsWith('.'))
|
|
150
|
+
continue;
|
|
151
|
+
let files;
|
|
152
|
+
try {
|
|
153
|
+
files = fs.readdirSync(path.join(root, folder.name));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
for (const name of files) {
|
|
159
|
+
if (!/\.html?$/i.test(name) || name.startsWith('.'))
|
|
160
|
+
continue;
|
|
161
|
+
const file = path.join(root, folder.name, name);
|
|
162
|
+
try {
|
|
163
|
+
const st = fs.lstatSync(file);
|
|
164
|
+
if (!st.isFile())
|
|
165
|
+
continue;
|
|
166
|
+
const head = readHead(file);
|
|
167
|
+
const title = /<title>([^<]*)<\/title>/i.exec(head)?.[1]?.trim();
|
|
168
|
+
const kind = meta(head, 'eklavya:kind');
|
|
169
|
+
rows.push({
|
|
170
|
+
id: `${folder.name}/${name}`,
|
|
171
|
+
title: title ? unescHtml(title) : name.replace(/\.html?$/i, ''),
|
|
172
|
+
description: meta(head, 'description') ?? '',
|
|
173
|
+
project: meta(head, 'eklavya:project') || null,
|
|
174
|
+
kind: kind === 'explainer' ? 'explainer' : 'artifact',
|
|
175
|
+
concept: meta(head, 'eklavya:concept') || null,
|
|
176
|
+
created: meta(head, 'eklavya:created') || st.mtime.toISOString(),
|
|
177
|
+
bytes: st.size,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return rows.sort((a, b) => (a.created < b.created ? 1 : a.created > b.created ? -1 : 0));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The file behind an id, or null. Exactly `<folder>/<file>.html`, both plain
|
|
189
|
+
* names — no separators, no `..`, no leading dot — and the resolved path must
|
|
190
|
+
* still sit inside the root after symlinks, so a link planted in a project
|
|
191
|
+
* folder cannot serve a file from elsewhere on the machine.
|
|
192
|
+
*/
|
|
193
|
+
export function resolveArtifact(id, root = artifactsDir()) {
|
|
194
|
+
const parts = id.split('/');
|
|
195
|
+
if (parts.length !== 2)
|
|
196
|
+
return null;
|
|
197
|
+
const plain = /^[^/\\\0]+$/;
|
|
198
|
+
for (const p of parts)
|
|
199
|
+
if (!plain.test(p) || p.startsWith('.'))
|
|
200
|
+
return null;
|
|
201
|
+
if (!/\.html?$/i.test(parts[1]))
|
|
202
|
+
return null;
|
|
203
|
+
try {
|
|
204
|
+
const realRoot = fs.realpathSync(root);
|
|
205
|
+
const real = fs.realpathSync(path.join(root, parts[0], parts[1]));
|
|
206
|
+
if (path.dirname(path.dirname(real)) !== realRoot)
|
|
207
|
+
return null;
|
|
208
|
+
return fs.statSync(real).isFile() ? real : null;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=artifacts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{{TITLE}}</title>
|
|
7
|
+
<meta name="description" content="{{DESCRIPTION}}">
|
|
8
|
+
<meta name="eklavya:project" content="{{PROJECT}}">
|
|
9
|
+
<meta name="eklavya:kind" content="{{KIND}}">
|
|
10
|
+
<meta name="eklavya:concept" content="{{CONCEPT}}">
|
|
11
|
+
<meta name="eklavya:created" content="{{CREATED}}">
|
|
12
|
+
<!-- Eklavya type. Offline, the stacks fall back to system faces; nothing breaks. -->
|
|
13
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
14
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
15
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;800;900&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap">
|
|
16
|
+
<style>
|
|
17
|
+
/* Eklavya design tokens, inlined from web/public/tokens.css when this page was
|
|
18
|
+
created. Ink (dark) is the default on screen and in the PDF; data-mode="paper"
|
|
19
|
+
on <html> switches to the paper ground. Components below name ROLES only. */
|
|
20
|
+
/*{{TOKENS}}*/
|
|
21
|
+
</style>
|
|
22
|
+
<style>
|
|
23
|
+
:root{ --ok:var(--spot); --maxw:880px; }
|
|
24
|
+
|
|
25
|
+
*{box-sizing:border-box}
|
|
26
|
+
body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.65 var(--font-body)}
|
|
27
|
+
::selection{background:var(--spot);color:var(--spot-ink)}
|
|
28
|
+
main{max-width:var(--maxw);margin:0 auto;padding:64px 16px 96px}
|
|
29
|
+
|
|
30
|
+
.eyebrow{margin:0 0 14px;font:400 11px/1 var(--font-mono);letter-spacing:var(--tracking-caps-wide);
|
|
31
|
+
text-transform:uppercase;color:var(--spot)}
|
|
32
|
+
h1{font:900 clamp(34px,5.4vw,64px)/.95 var(--font-disp);letter-spacing:var(--tracking-display);margin:0 0 .35em}
|
|
33
|
+
h2{font:800 clamp(24px,3vw,34px)/1 var(--font-disp);letter-spacing:var(--tracking-tight);
|
|
34
|
+
margin:2.4em 0 .7em;padding-top:1em;border-top:1px solid var(--line-2)}
|
|
35
|
+
h3{font:800 18px/1.3 var(--font-disp);letter-spacing:var(--tracking-snug);margin:1.8em 0 .4em}
|
|
36
|
+
p,li{color:var(--ink)}
|
|
37
|
+
.lede{color:var(--dim);font-size:17px;max-width:620px;margin:0 0 2.4em}
|
|
38
|
+
.muted{color:var(--dim)}
|
|
39
|
+
a{color:var(--spot);text-decoration:none;border-bottom:1px solid var(--spot)}
|
|
40
|
+
strong{font-weight:600}
|
|
41
|
+
|
|
42
|
+
code{font-family:var(--font-mono);font-size:.88em;background:var(--mass);padding:.1em .38em}
|
|
43
|
+
pre{background:var(--panel);border:1px solid var(--line-2);padding:14px 16px;overflow:auto;font-size:13px}
|
|
44
|
+
pre code{background:none;padding:0}
|
|
45
|
+
|
|
46
|
+
table{width:100%;border-collapse:collapse;margin:1.2em 0;font-size:14px}
|
|
47
|
+
th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--line-2);vertical-align:top}
|
|
48
|
+
th{font:400 11px var(--font-mono);letter-spacing:var(--tracking-caps);text-transform:uppercase;color:var(--faint)}
|
|
49
|
+
td.num,th.num{text-align:right;font-family:var(--font-mono);font-variant-numeric:tabular-nums}
|
|
50
|
+
blockquote{margin:1.2em 0;padding:.2em 0 .2em 16px;border-left:2px solid var(--spot);color:var(--dim)}
|
|
51
|
+
|
|
52
|
+
.card{background:var(--panel);border:1px solid var(--line);padding:24px}
|
|
53
|
+
.card h3{margin-top:0}
|
|
54
|
+
.card p{color:var(--dim);font-size:14.5px;margin:0}
|
|
55
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin:1.4em 0}
|
|
56
|
+
.chip{display:inline-block;font:12px var(--font-mono);color:var(--spot);background:var(--spot-soft);
|
|
57
|
+
border:1px solid var(--spot);padding:2px 9px}
|
|
58
|
+
.chip.warn{color:var(--warning);border-color:var(--warning);background:color-mix(in srgb,var(--warning) 12%,transparent)}
|
|
59
|
+
.chip.bad{color:var(--error);border-color:var(--error);background:color-mix(in srgb,var(--error) 12%,transparent)}
|
|
60
|
+
.stat{border:1px solid var(--line-2);padding:18px 20px}
|
|
61
|
+
.stat .k{font:400 11px var(--font-mono);letter-spacing:var(--tracking-caps);text-transform:uppercase;color:var(--faint)}
|
|
62
|
+
.stat .v{font:900 40px/1.1 var(--font-disp);letter-spacing:var(--tracking-tight);margin-top:6px}
|
|
63
|
+
.stat .d{color:var(--dim);font-size:13px}
|
|
64
|
+
.callout{border:1px solid var(--spot);background:var(--spot-soft);padding:14px 18px;margin:1.4em 0}
|
|
65
|
+
|
|
66
|
+
figure{margin:1.8em 0;background:var(--panel);border:1px solid var(--line-2);
|
|
67
|
+
padding:20px;text-align:center;break-inside:avoid}
|
|
68
|
+
figure svg{max-width:100%;height:auto}
|
|
69
|
+
figcaption{margin-top:12px;color:var(--dim);font-size:13px;text-align:center}
|
|
70
|
+
.svg-ink{fill:var(--ink)} .svg-muted{fill:var(--dim)}
|
|
71
|
+
.svg-box{fill:var(--mass);stroke:var(--line-2);stroke-width:1}
|
|
72
|
+
.svg-box-spot{fill:var(--spot-soft);stroke:var(--spot);stroke-width:1.5}
|
|
73
|
+
.svg-line{stroke:var(--faint);stroke-width:1.5;fill:none}
|
|
74
|
+
.svg-line-spot{stroke:var(--spot);stroke-width:1.5;fill:none}
|
|
75
|
+
.svg-label{font:500 13px var(--font-body);fill:var(--ink)}
|
|
76
|
+
.svg-small{font:11px var(--font-mono);fill:var(--dim)}
|
|
77
|
+
|
|
78
|
+
.actions{position:fixed;top:16px;right:16px;z-index:50;display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}
|
|
79
|
+
.actions button{display:inline-flex;gap:7px;align-items:center;padding:8px 14px;
|
|
80
|
+
border:1px solid var(--line-2);border-radius:0;background:var(--panel);color:var(--ink);
|
|
81
|
+
font:500 12px var(--font-mono);letter-spacing:.06em;cursor:pointer}
|
|
82
|
+
.actions button:hover{border-color:var(--spot);color:var(--spot)}
|
|
83
|
+
.actions button:focus-visible{outline:none;box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--spot)}
|
|
84
|
+
|
|
85
|
+
@media screen{
|
|
86
|
+
body{background-image:linear-gradient(var(--grid) 1px,transparent 1px),
|
|
87
|
+
linear-gradient(90deg,var(--grid) 1px,transparent 1px);background-size:72px 72px}
|
|
88
|
+
main{background:var(--bg);border-left:1px solid var(--line-2);border-right:1px solid var(--line-2);
|
|
89
|
+
padding-left:40px;padding-right:40px;min-height:100vh}
|
|
90
|
+
}
|
|
91
|
+
@media (max-width:640px){ main{padding:76px 16px 64px!important} .actions{top:12px;right:12px} }
|
|
92
|
+
|
|
93
|
+
@media print{
|
|
94
|
+
/* Dark in the PDF too. The @page margin is canvas and prints white, so the
|
|
95
|
+
page has none, a fixed layer covers the sheet, and the gutter lives on
|
|
96
|
+
<main>, repeated on every page fragment by box-decoration-break. */
|
|
97
|
+
*{-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
|
98
|
+
html,body{background:var(--bg);color:var(--ink)}
|
|
99
|
+
@page{margin:0}
|
|
100
|
+
body::before{content:"";position:fixed;inset:0;background:var(--bg);z-index:-1}
|
|
101
|
+
.actions{display:none}
|
|
102
|
+
body{font-size:11pt}
|
|
103
|
+
main{padding:12mm 16mm;max-width:none;-webkit-box-decoration-break:clone;box-decoration-break:clone}
|
|
104
|
+
h1,h2,h3{break-after:avoid;break-inside:avoid}
|
|
105
|
+
p,li{orphans:3;widows:3}
|
|
106
|
+
/* Anything that can outgrow a page must be allowed to split. */
|
|
107
|
+
table,pre,section,div{break-inside:auto}
|
|
108
|
+
thead{display:table-header-group}
|
|
109
|
+
tfoot{display:table-footer-group}
|
|
110
|
+
tr,td,th,li,.card,.stat{break-inside:avoid}
|
|
111
|
+
figure,blockquote{break-inside:avoid}
|
|
112
|
+
figure svg,figure img{max-height:170mm}
|
|
113
|
+
a{color:inherit;border:0}
|
|
114
|
+
}
|
|
115
|
+
</style>
|
|
116
|
+
</head>
|
|
117
|
+
<body>
|
|
118
|
+
<div class="actions">
|
|
119
|
+
<button type="button" onclick="window.print()" aria-label="Download this page as PDF">PDF</button>
|
|
120
|
+
<button type="button" onclick="saveHtml()" aria-label="Download this page as an HTML file">HTML</button>
|
|
121
|
+
</div>
|
|
122
|
+
<script>
|
|
123
|
+
function saveHtml(){
|
|
124
|
+
var name=(location.pathname.split('/').pop()||'').replace(/\.html?$/,'')||'artifact';
|
|
125
|
+
var blob=new Blob(['<!DOCTYPE html>\n'+document.documentElement.outerHTML],{type:'text/html'});
|
|
126
|
+
var a=document.createElement('a');
|
|
127
|
+
a.href=URL.createObjectURL(blob); a.download=name+'.html';
|
|
128
|
+
document.body.appendChild(a); a.click(); a.remove();
|
|
129
|
+
setTimeout(function(){URL.revokeObjectURL(a.href)},1000);
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<main>
|
|
134
|
+
<p class="eyebrow">{{EYEBROW}} · {{PROJECT_NAME}}</p>
|
|
135
|
+
<h1>{{TITLE}}</h1>
|
|
136
|
+
<p class="lede">{{DESCRIPTION}}</p>
|
|
137
|
+
|
|
138
|
+
<!-- CONTENT: replace this comment with the page. Sections under <h2>, a
|
|
139
|
+
<figure> with an inline SVG (the .svg-* classes) for every sequence,
|
|
140
|
+
branch or before/after, a <figcaption> that states the takeaway. -->
|
|
141
|
+
|
|
142
|
+
</main>
|
|
143
|
+
</body>
|
|
144
|
+
</html>
|
|
@@ -566,6 +566,7 @@ const T = {
|
|
|
566
566
|
session: { page:1 },
|
|
567
567
|
memory: { page:1, q:'', tag:'all', since:'', until:'' },
|
|
568
568
|
reuse: { page:1, only:'all' },
|
|
569
|
+
artifacts:{ page:1, q:'' },
|
|
569
570
|
};
|
|
570
571
|
const PER = 20;
|
|
571
572
|
|
|
@@ -2162,8 +2163,8 @@ function viewHealth() {
|
|
|
2162
2163
|
/* ============================================================
|
|
2163
2164
|
Workflows and routes
|
|
2164
2165
|
|
|
2165
|
-
|
|
2166
|
-
|
|
2166
|
+
Three workflows, one shell. The registry below is the only description of
|
|
2167
|
+
any of them: the sidebar, the picker, the route resolver and the page renderer all
|
|
2167
2168
|
read it, so the selector, the highlighted item and the screen cannot disagree
|
|
2168
2169
|
about where the reader is.
|
|
2169
2170
|
|
|
@@ -2171,7 +2172,106 @@ function viewHealth() {
|
|
|
2171
2172
|
workflow, the page, the highlighted item and the project scope, so a refresh,
|
|
2172
2173
|
a pasted link or Back/Forward rebuilds the same screen from nothing.
|
|
2173
2174
|
============================================================ */
|
|
2175
|
+
/* ============================================================
|
|
2176
|
+
Artifacts — the pages an agent wrote under ~/.eklavya/artifacts/.
|
|
2177
|
+
`S.artifacts` is read from the files on every load; opening one is a plain
|
|
2178
|
+
link to `/artifacts/<id>`, which the server serves sandboxed, in a new tab.
|
|
2179
|
+
============================================================ */
|
|
2180
|
+
const ARTIFACT_KINDS = ['all', 'explainer', 'artifact'];
|
|
2181
|
+
const artifactsInScope = () => (S.artifacts ?? []).filter((a) => inScope(a.project));
|
|
2182
|
+
const artifactUrl = (a) => '/artifacts/' + a.id.split('/').map(encodeURIComponent).join('/');
|
|
2183
|
+
|
|
2184
|
+
function artifactRow(a) {
|
|
2185
|
+
const known = a.concept && S.concepts.some((c) => c.slug === a.concept);
|
|
2186
|
+
const concept = !a.concept ? '<span class="slug">—</span>'
|
|
2187
|
+
: known ? `<a class="link slug" data-go="${esc(LH('concept', a.concept))}">${esc(a.concept)}</a>`
|
|
2188
|
+
: `<span class="slug">${esc(a.concept)}</span>`;
|
|
2189
|
+
return `<tr>
|
|
2190
|
+
<td style="min-width:260px"><a class="link title" href="${esc(artifactUrl(a))}" target="_blank" rel="noopener">${esc(a.title)}</a>
|
|
2191
|
+
${a.description ? `<div class="ctx">${esc(a.description)}</div>` : ''}</td>
|
|
2192
|
+
<td><span class="pill">${esc(a.kind)}</span></td>
|
|
2193
|
+
<td>${concept}</td>
|
|
2194
|
+
<td class="num slug" style="white-space:nowrap">${esc(stamp(a.created))}</td></tr>`;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
function viewArtifacts(param) {
|
|
2198
|
+
const t = T.artifacts;
|
|
2199
|
+
const kind = ARTIFACT_KINDS.includes(param) ? param : 'all';
|
|
2200
|
+
const all = artifactsInScope();
|
|
2201
|
+
const q = t.q.trim().toLowerCase();
|
|
2202
|
+
const list = all.filter((a) => {
|
|
2203
|
+
if (kind !== 'all' && a.kind !== kind) return false;
|
|
2204
|
+
if (q && ![a.title, a.description, a.concept ?? '', pname(a.project), a.id].join(' ').toLowerCase().includes(q)) return false;
|
|
2205
|
+
return true;
|
|
2206
|
+
});
|
|
2207
|
+
const p = page('artifacts', list.length);
|
|
2208
|
+
// The page's slice, grouped by project in the order it arrives (newest first).
|
|
2209
|
+
const groups = new Map();
|
|
2210
|
+
for (const a of slice(list, p)) {
|
|
2211
|
+
const id = pid(a.project);
|
|
2212
|
+
if (!groups.has(id)) groups.set(id, []);
|
|
2213
|
+
groups.get(id).push(a);
|
|
2214
|
+
}
|
|
2215
|
+
const counts = { all: all.length, explainer: all.filter((a) => a.kind === 'explainer').length };
|
|
2216
|
+
counts.artifact = all.length - counts.explainer;
|
|
2217
|
+
const chip = (k, label) => `<button class="chip" data-go="${esc(AH('dashboard', k === 'all' ? null : k))}" aria-pressed="${kind === k}">${label} ${counts[k]}</button>`;
|
|
2218
|
+
const cols = [{t:'Page'},{t:'Kind'},{t:'Concept'},{t:'Written',n:1}];
|
|
2219
|
+
const body = !list.length
|
|
2220
|
+
? `<p class="empty">${all.length ? 'Nothing matches that search.' : 'No artifacts yet. Ask for an Eklavya artifact, or turn on explain_on_wrong, and they land here.'}</p>`
|
|
2221
|
+
: [...groups].map(([id, rows]) =>
|
|
2222
|
+
`<h3 class="slug" style="margin:var(--space-5) 0 var(--space-2)">${esc(projectById(id)?.name ?? (id === '~' ? 'no project' : base(id)))}</h3>`
|
|
2223
|
+
+ table(cols, rows.map(artifactRow))).join('') + pager('artifacts', list.length, p);
|
|
2224
|
+
return [
|
|
2225
|
+
`<div>
|
|
2226
|
+
<h1 class="page__title">Artifacts</h1>
|
|
2227
|
+
<p class="page__sub">Pages your agent wrote for you — explainers after a missed question, and anything you asked
|
|
2228
|
+
to have turned into a page. Each opens in a new tab, from <code>~/.eklavya/artifacts/</code>.</p>
|
|
2229
|
+
</div>`,
|
|
2230
|
+
tiles([
|
|
2231
|
+
['Artifacts', all.length, 'in scope'],
|
|
2232
|
+
['Explainers', counts.explainer, 'written after a question', null, counts.explainer ? AH('dashboard', 'explainer') : null],
|
|
2233
|
+
['Projects', new Set(all.map((a) => pid(a.project))).size, 'with at least one page', null, AH('projects')],
|
|
2234
|
+
]),
|
|
2235
|
+
card(null, null,
|
|
2236
|
+
`<div class="filters">
|
|
2237
|
+
<div class="grow"><input type="search" id="aq" value="${esc(t.q)}" placeholder="Search title, description, concept or project…" aria-label="Search artifacts"></div>
|
|
2238
|
+
</div>
|
|
2239
|
+
<div class="chips">${chip('all','All')}${chip('explainer','Explainers')}${chip('artifact','Other pages')}</div>
|
|
2240
|
+
<div style="margin-top:var(--space-4)">${body}</div>`),
|
|
2241
|
+
].join('');
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
function viewArtifactProjects() {
|
|
2245
|
+
const by = new Map();
|
|
2246
|
+
for (const a of artifactsInScope()) {
|
|
2247
|
+
const id = pid(a.project);
|
|
2248
|
+
const r = by.get(id) ?? { id, n: 0, explainers: 0, last: a.created };
|
|
2249
|
+
r.n += 1; if (a.kind === 'explainer') r.explainers += 1;
|
|
2250
|
+
if (a.created > r.last) r.last = a.created;
|
|
2251
|
+
by.set(id, r);
|
|
2252
|
+
}
|
|
2253
|
+
// ponytail: a project the inventory has never seen (a directory with no
|
|
2254
|
+
// Eklavya activity) cannot be a scope, so its row is not a link. Upgrade:
|
|
2255
|
+
// teach `projectInventory` to count artifact projects.
|
|
2256
|
+
const rows = [...by.values()].sort((a, b) => (a.last < b.last ? 1 : -1)).map((r) => {
|
|
2257
|
+
const p = projectById(r.id);
|
|
2258
|
+
return `<tr${p ? ` class="row" data-go="${esc(AH('dashboard', null, { project: r.id }))}"` : ''}>
|
|
2259
|
+
<td style="min-width:220px"><span class="title">${esc(p?.name ?? base(r.id))}</span><div class="slug">${esc(r.id)}</div></td>
|
|
2260
|
+
<td class="num">${r.n}</td><td class="num">${r.explainers || '—'}</td>
|
|
2261
|
+
<td class="num slug">${esc(ago(r.last))}</td></tr>`;
|
|
2262
|
+
});
|
|
2263
|
+
return [
|
|
2264
|
+
`<div>
|
|
2265
|
+
<h1 class="page__title">Projects</h1>
|
|
2266
|
+
<p class="page__sub">Artifacts are filed by the project they were written in — the main checkout, for a worktree.
|
|
2267
|
+
Open one to see only its pages.</p>
|
|
2268
|
+
</div>`,
|
|
2269
|
+
card(null, null, table([{t:'Project'},{t:'Pages',n:1},{t:'Explainers',n:1},{t:'Last written',n:1}], rows)),
|
|
2270
|
+
].join('');
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2174
2273
|
const ICONS = {
|
|
2274
|
+
artifacts: '<path d="M6 3h9l4 4v14H6z"/><path d="M14 3v5h5"/><path d="M9 13h7M9 17h5"/>',
|
|
2175
2275
|
learning: '<path d="M4 19V6a2 2 0 0 1 2-2h13v13H6a2 2 0 0 0-2 2z"/><path d="M6 21h13v-4"/><path d="M9 8h6"/>',
|
|
2176
2276
|
memory: '<ellipse cx="12" cy="6" rx="8" ry="3"/><path d="M4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6"/><path d="M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"/>',
|
|
2177
2277
|
dashboard: '<path d="M3 12l9-9 9 9"/><path d="M5 10v10h14V10"/>',
|
|
@@ -2230,6 +2330,16 @@ const WORKFLOWS = {
|
|
|
2230
2330
|
health: { label: 'Health', view: () => viewHealth() },
|
|
2231
2331
|
},
|
|
2232
2332
|
},
|
|
2333
|
+
artifacts: {
|
|
2334
|
+
name: 'Artifacts',
|
|
2335
|
+
groups: [
|
|
2336
|
+
{ id: 'library', label: 'Library', items: ['projects'] },
|
|
2337
|
+
],
|
|
2338
|
+
pages: {
|
|
2339
|
+
dashboard: { label: 'Dashboard', view: (p) => viewArtifacts(p) },
|
|
2340
|
+
projects: { label: 'Projects', view: () => viewArtifactProjects() },
|
|
2341
|
+
},
|
|
2342
|
+
},
|
|
2233
2343
|
};
|
|
2234
2344
|
|
|
2235
2345
|
/**
|
|
@@ -2247,6 +2357,7 @@ function href(wf, page, param, q = {}) {
|
|
|
2247
2357
|
}
|
|
2248
2358
|
const LH = (page, param, q) => href('learning', page, param, q);
|
|
2249
2359
|
const MH = (page, param, q) => href('memory', page, param, q);
|
|
2360
|
+
const AH = (page, param, q) => href('artifacts', page, param, q);
|
|
2250
2361
|
/** A session opened from Memory stays in Memory, and from Learning in Learning. */
|
|
2251
2362
|
const sessHref = (id) => href(CUR.wf, 'session', id);
|
|
2252
2363
|
|
|
@@ -2355,6 +2466,7 @@ function navCounts(wf) {
|
|
|
2355
2466
|
domains: new Set(conceptsInScope().map((c) => c.domain)).size,
|
|
2356
2467
|
};
|
|
2357
2468
|
}
|
|
2469
|
+
if (wf === 'artifacts') return { projects: new Set(artifactsInScope().map((a) => pid(a.project))).size };
|
|
2358
2470
|
return {
|
|
2359
2471
|
timeline: sum((p) => p.memory.entries),
|
|
2360
2472
|
sessions: sum((p) => p.memory.sessions),
|
|
@@ -2694,6 +2806,11 @@ $('view').addEventListener('input', (e) => {
|
|
|
2694
2806
|
T.concepts.page = 1;
|
|
2695
2807
|
render({ id:'q', pos:e.target.selectionStart });
|
|
2696
2808
|
}
|
|
2809
|
+
if (e.target.id === 'aq') {
|
|
2810
|
+
T.artifacts.q = e.target.value;
|
|
2811
|
+
T.artifacts.page = 1;
|
|
2812
|
+
render({ id:'aq', pos:e.target.selectionStart });
|
|
2813
|
+
}
|
|
2697
2814
|
if (e.target.id === 'mq') {
|
|
2698
2815
|
T.memory.q = e.target.value;
|
|
2699
2816
|
T.memory.page = 1;
|
|
@@ -168,6 +168,13 @@ tool exists to prevent. **Read `references/grading.md` before you grade** — bo
|
|
|
168
168
|
scales, how long feedback may be, the sequence a blank earns, and what
|
|
169
169
|
`already_taught` changes.
|
|
170
170
|
|
|
171
|
+
**A missed answer can come back with `explain`.** That is `explain_on_wrong` at
|
|
172
|
+
work: follow its `instruction` exactly. The page is written in the background
|
|
173
|
+
and opens by itself, so give the verdict, say the page is on its way, and go
|
|
174
|
+
back to the task — no longer explanation, no waiting. When the developer asks
|
|
175
|
+
for something to be explained as a page ("explain this to me", "make me a page
|
|
176
|
+
on this"), start the same background explainer whatever the setting says.
|
|
177
|
+
|
|
171
178
|
## The dials
|
|
172
179
|
|
|
173
180
|
**Mode** is how hard to push, **focus** is what to teach, **cadence** is when to
|
package/dist/cli-memory.js
CHANGED
|
@@ -451,11 +451,14 @@ function memoryPrune() {
|
|
|
451
451
|
try {
|
|
452
452
|
const { config } = loadConfig();
|
|
453
453
|
if (!config.memory.retention_days) {
|
|
454
|
-
process.stdout.write('memory.retention_days is not set, so raw evidence is kept until deleted by hand.\n');
|
|
454
|
+
process.stdout.write('memory.retention_days is not set for this project, so its raw evidence is kept until deleted by hand.\n');
|
|
455
455
|
return;
|
|
456
456
|
}
|
|
457
|
-
|
|
458
|
-
|
|
457
|
+
// This project only, under this project's resolved retention: another
|
|
458
|
+
// project may keep its evidence (retention unset) or a different window.
|
|
459
|
+
const project = currentProject();
|
|
460
|
+
const removed = pruneEvidence(db, config, { project });
|
|
461
|
+
process.stdout.write(`Deleted ${removed} raw evidence events older than ${config.memory.retention_days} days in ${project}.\n`);
|
|
459
462
|
}
|
|
460
463
|
finally {
|
|
461
464
|
db.close();
|