syndes 0.1.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 +21 -0
- package/README.md +77 -0
- package/adapters/claude-code.mjs +59 -0
- package/adapters/codex.mjs +256 -0
- package/adapters/index.mjs +92 -0
- package/analytics/index.mjs +189 -0
- package/analytics/metrics/context.mjs +95 -0
- package/analytics/metrics/cost.mjs +83 -0
- package/analytics/metrics/friction.mjs +86 -0
- package/analytics/metrics/prompts.mjs +93 -0
- package/analytics/metrics/rework.mjs +113 -0
- package/analytics/metrics/time.mjs +104 -0
- package/analytics/metrics/tokens.mjs +88 -0
- package/analytics/metrics/tools.mjs +118 -0
- package/analytics/metrics/volume.mjs +98 -0
- package/analytics/ranges.mjs +98 -0
- package/analytics/rollup.mjs +151 -0
- package/analytics/score.mjs +194 -0
- package/bin/cli.mjs +596 -0
- package/bin/postinstall.mjs +44 -0
- package/collect/classify.mjs +226 -0
- package/collect/git.mjs +78 -0
- package/collect/projects.mjs +82 -0
- package/collect/redact.mjs +85 -0
- package/collect/sessions.mjs +119 -0
- package/collect/tail.mjs +126 -0
- package/collect/tools.mjs +121 -0
- package/collect/transcript.mjs +128 -0
- package/dashboard/api/index.mjs +296 -0
- package/dashboard/auth.mjs +235 -0
- package/dashboard/router.mjs +55 -0
- package/dashboard/security.mjs +95 -0
- package/dashboard/server.mjs +156 -0
- package/dashboard/static.mjs +47 -0
- package/dashboard/web/SynDes.icns +0 -0
- package/dashboard/web/api.js +80 -0
- package/dashboard/web/app.css +532 -0
- package/dashboard/web/app.js +261 -0
- package/dashboard/web/charts.js +273 -0
- package/dashboard/web/index.html +23 -0
- package/dashboard/web/logo.png +0 -0
- package/dashboard/web/ui.js +434 -0
- package/dashboard/web/views/habits.js +166 -0
- package/dashboard/web/views/ledger.js +164 -0
- package/dashboard/web/views/overview.js +214 -0
- package/dashboard/web/views/sessions.js +133 -0
- package/dashboard/web/views/settings.js +180 -0
- package/ledger/append.mjs +126 -0
- package/ledger/chain.mjs +53 -0
- package/ledger/keys.mjs +72 -0
- package/ledger/read.mjs +77 -0
- package/ledger/retention.mjs +104 -0
- package/ledger/schema.mjs +96 -0
- package/ledger/segments.mjs +109 -0
- package/ledger/verify.mjs +174 -0
- package/notify/index.mjs +67 -0
- package/notify/linux.mjs +41 -0
- package/notify/mac.mjs +44 -0
- package/notify/terminal.mjs +15 -0
- package/notify/windows.mjs +61 -0
- package/package.json +66 -0
- package/practices/budget.mjs +97 -0
- package/practices/catalog.mjs +64 -0
- package/practices/deliver.mjs +101 -0
- package/practices/engine.mjs +107 -0
- package/practices/rules/batch-tool-calls.mjs +15 -0
- package/practices/rules/context-hygiene.mjs +17 -0
- package/practices/rules/delegate-wide-search.mjs +15 -0
- package/practices/rules/index.mjs +28 -0
- package/practices/rules/permission-friction.mjs +16 -0
- package/practices/rules/project-memory.mjs +27 -0
- package/practices/rules/prompt-specificity.mjs +15 -0
- package/practices/rules/read-before-edit.mjs +16 -0
- package/practices/rules/retry-storm.mjs +22 -0
- package/practices/rules/session-sprawl.mjs +15 -0
- package/practices/rules/verify-after-change.mjs +16 -0
- package/runtime/config.mjs +116 -0
- package/runtime/hook.mjs +154 -0
- package/runtime/jsonl.mjs +104 -0
- package/runtime/lock.mjs +98 -0
- package/runtime/log.mjs +37 -0
- package/runtime/paths.mjs +116 -0
- package/runtime/platform.mjs +74 -0
- package/runtime/spool.mjs +92 -0
- package/runtime/worker.mjs +275 -0
- package/src/briefing.mjs +94 -0
- package/src/doctor.mjs +153 -0
- package/src/export.mjs +68 -0
- package/src/install.mjs +95 -0
- package/src/open.mjs +23 -0
- package/src/report.mjs +120 -0
- package/src/settings.mjs +173 -0
- package/src/status.mjs +61 -0
- package/src/systemauth.mjs +179 -0
- package/src/term.mjs +272 -0
- package/src/uninstall.mjs +43 -0
package/src/term.mjs
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colours, boxes, tables and prompts.
|
|
3
|
+
*
|
|
4
|
+
* Honours NO_COLOR and a non-TTY stdout, and falls back to ASCII where the code
|
|
5
|
+
* page cannot draw box characters — a garbled report on Windows Terminal is a
|
|
6
|
+
* bug report we should not have to receive.
|
|
7
|
+
*
|
|
8
|
+
* The escape byte is built with fromCharCode rather than written literally, so
|
|
9
|
+
* this source file stays free of control characters and survives every editor,
|
|
10
|
+
* diff viewer and patch tool it will pass through.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createInterface } from 'node:readline';
|
|
14
|
+
import { isWindows } from '../runtime/platform.mjs';
|
|
15
|
+
|
|
16
|
+
const ESC = String.fromCharCode(27);
|
|
17
|
+
const CSI = `${ESC}[`;
|
|
18
|
+
const ANSI = new RegExp(`${ESC}\\[[0-9;]*m`, 'g');
|
|
19
|
+
|
|
20
|
+
const COLOR = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && process.env.TERM !== 'dumb';
|
|
21
|
+
|
|
22
|
+
/** Windows consoles on a legacy code page render box drawing as mojibake. */
|
|
23
|
+
const UNICODE = !isWindows || Boolean(process.env.WT_SESSION) || Boolean(process.env.TERM_PROGRAM);
|
|
24
|
+
|
|
25
|
+
const wrap = (code) => (text) => (COLOR ? `${CSI}${code}m${text}${CSI}0m` : String(text));
|
|
26
|
+
|
|
27
|
+
export const bold = wrap(1);
|
|
28
|
+
export const dim = wrap(2);
|
|
29
|
+
export const red = wrap(31);
|
|
30
|
+
export const green = wrap(32);
|
|
31
|
+
export const yellow = wrap(33);
|
|
32
|
+
export const blue = wrap(34);
|
|
33
|
+
export const magenta = wrap(35);
|
|
34
|
+
export const cyan = wrap(36);
|
|
35
|
+
export const grey = wrap(90);
|
|
36
|
+
|
|
37
|
+
export const OK = green(UNICODE ? '✓' : 'OK');
|
|
38
|
+
export const FAIL = red(UNICODE ? '✗' : 'X');
|
|
39
|
+
export const WARN = yellow('!');
|
|
40
|
+
export const DOT = UNICODE ? '·' : '-';
|
|
41
|
+
|
|
42
|
+
export const BOX = UNICODE
|
|
43
|
+
? { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│', lt: '├', rt: '┤' }
|
|
44
|
+
: { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|', lt: '+', rt: '+' };
|
|
45
|
+
|
|
46
|
+
export function write(text = '') {
|
|
47
|
+
process.stdout.write(`${text}\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const width = () => Math.min(process.stdout.columns || 80, 100);
|
|
51
|
+
|
|
52
|
+
/** Visible length, ignoring colour escapes — padEnd on a coloured string lies. */
|
|
53
|
+
export function visible(text) {
|
|
54
|
+
return String(text).replace(ANSI, '').length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function pad(text, size) {
|
|
58
|
+
const gap = size - visible(text);
|
|
59
|
+
return gap > 0 ? text + ' '.repeat(gap) : text;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function padStart(text, size) {
|
|
63
|
+
const gap = size - visible(text);
|
|
64
|
+
return gap > 0 ? ' '.repeat(gap) + text : text;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function rule(size = width()) {
|
|
68
|
+
return grey(BOX.h.repeat(size));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function table(rows, { align = [] } = {}) {
|
|
72
|
+
if (!rows.length) return [];
|
|
73
|
+
const columns = Math.max(...rows.map((row) => row.length));
|
|
74
|
+
const widths = [];
|
|
75
|
+
for (let column = 0; column < columns; column += 1) {
|
|
76
|
+
widths.push(Math.max(...rows.map((row) => visible(row[column] ?? ''))));
|
|
77
|
+
}
|
|
78
|
+
return rows.map((row) => row
|
|
79
|
+
.map((cell, column) => (align[column] === 'right'
|
|
80
|
+
? padStart(cell ?? '', widths[column])
|
|
81
|
+
: pad(cell ?? '', widths[column])))
|
|
82
|
+
.join(' ')
|
|
83
|
+
.trimEnd());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A horizontal bar for proportions. Never the only channel carrying meaning. */
|
|
87
|
+
export function bar(fraction, size = 20, colour = cyan) {
|
|
88
|
+
const filled = Math.max(0, Math.min(size, Math.round((fraction ?? 0) * size)));
|
|
89
|
+
const glyph = UNICODE ? '█' : '#';
|
|
90
|
+
const empty = UNICODE ? '░' : '.';
|
|
91
|
+
return colour(glyph.repeat(filled)) + grey(empty.repeat(size - filled));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function sparkline(values) {
|
|
95
|
+
if (!values.length) return '';
|
|
96
|
+
const glyphs = UNICODE ? '▁▂▃▄▅▆▇█' : '.:-=+*#@';
|
|
97
|
+
const max = Math.max(...values, 1);
|
|
98
|
+
return values
|
|
99
|
+
.map((value) => glyphs[Math.min(glyphs.length - 1, Math.floor((value / max) * (glyphs.length - 1)))])
|
|
100
|
+
.join('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A spinner for work that takes long enough to notice.
|
|
105
|
+
*
|
|
106
|
+
* Animation is a courtesy, not a requirement: when stdout is not a TTY — piped
|
|
107
|
+
* to a file, running in CI, read by another program — this degrades to one
|
|
108
|
+
* printed line per step, because writing cursor escapes into a log file
|
|
109
|
+
* produces garbage nobody can read.
|
|
110
|
+
*/
|
|
111
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
112
|
+
|
|
113
|
+
export function spinner(label) {
|
|
114
|
+
const animated = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && !process.env.CI;
|
|
115
|
+
let frame = 0;
|
|
116
|
+
let text = label;
|
|
117
|
+
let timer = null;
|
|
118
|
+
|
|
119
|
+
const paint = () => {
|
|
120
|
+
process.stdout.write(`\r${CSI}2K ${cyan(FRAMES[frame])} ${text}`);
|
|
121
|
+
frame = (frame + 1) % FRAMES.length;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (animated) {
|
|
125
|
+
paint();
|
|
126
|
+
timer = setInterval(paint, 80);
|
|
127
|
+
timer.unref?.();
|
|
128
|
+
}
|
|
129
|
+
// Non-TTY prints nothing on start: the result line says what happened, and
|
|
130
|
+
// emitting both turns a clean log into two lines per step saying the same thing.
|
|
131
|
+
|
|
132
|
+
const stop = (mark, message, detail) => {
|
|
133
|
+
if (timer) clearInterval(timer);
|
|
134
|
+
if (animated) process.stdout.write(`\r${CSI}2K`);
|
|
135
|
+
write(` ${mark} ${message}${detail ? ` ${grey(detail)}` : ''}`);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
update(next) { text = next; },
|
|
140
|
+
succeed: (detail) => stop(OK, text, detail),
|
|
141
|
+
skip: (detail) => stop(grey(DOT), grey(text), detail),
|
|
142
|
+
fail: (detail) => stop(FAIL, text, detail),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Print text progressively, the way a model streams tokens.
|
|
148
|
+
*
|
|
149
|
+
* Two things make this non-trivial. Colour escapes must never be split across
|
|
150
|
+
* writes — half of `\e[36m` on the wire is garbage on screen — so the text is
|
|
151
|
+
* tokenised with each escape sequence kept whole. And it degrades to an instant
|
|
152
|
+
* write whenever nobody is watching: a non-TTY, CI, or NO_COLOR. Streaming into
|
|
153
|
+
* a log file is just a slow way to produce the same bytes.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} text may already contain colour
|
|
156
|
+
* @param {{speed?: number}} options milliseconds between words
|
|
157
|
+
*/
|
|
158
|
+
export async function stream(text, { speed = 11 } = {}) {
|
|
159
|
+
const animated = Boolean(process.stdout.isTTY) && !process.env.CI && process.env.SYNDES_NO_ANIMATION !== '1';
|
|
160
|
+
if (!animated) return write(text);
|
|
161
|
+
|
|
162
|
+
for (const chunk of tokenise(text)) {
|
|
163
|
+
process.stdout.write(chunk);
|
|
164
|
+
// A line break is a beat: it is where a reader's eye moves anyway.
|
|
165
|
+
await pause(chunk.endsWith('\n') ? speed * 6 : speed);
|
|
166
|
+
}
|
|
167
|
+
process.stdout.write('\n');
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Split into word-sized chunks, never cutting an escape sequence in half. */
|
|
172
|
+
function tokenise(text) {
|
|
173
|
+
const out = [];
|
|
174
|
+
let buffer = '';
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
177
|
+
const char = text[i];
|
|
178
|
+
if (char === ESC) {
|
|
179
|
+
const end = text.indexOf('m', i);
|
|
180
|
+
if (end === -1) { buffer += char; continue; }
|
|
181
|
+
buffer += text.slice(i, end + 1);
|
|
182
|
+
i = end;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
buffer += char;
|
|
186
|
+
if (char === ' ' || char === '\n') { out.push(buffer); buffer = ''; }
|
|
187
|
+
}
|
|
188
|
+
if (buffer) out.push(buffer);
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function pause(ms) {
|
|
193
|
+
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The wordmark, for the top of a first run. */
|
|
197
|
+
export function banner() {
|
|
198
|
+
const mark = UNICODE ? '◆' : '*';
|
|
199
|
+
write();
|
|
200
|
+
write(` ${bold(cyan(mark))} ${bold('SynDes')} ${grey('· a record of how you work with coding agents')}`);
|
|
201
|
+
write();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Hold for a beat so a sequence of steps reads as progress rather than a dump. */
|
|
205
|
+
export function beat(ms = 90) {
|
|
206
|
+
if (!process.stdout.isTTY) return Promise.resolve();
|
|
207
|
+
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function ask(question, { silent = false } = {}) {
|
|
211
|
+
return new Promise((resolve) => {
|
|
212
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
213
|
+
|
|
214
|
+
if (silent) {
|
|
215
|
+
// Echo off by hand: readline has no password mode, and a password printed
|
|
216
|
+
// into a terminal that may be scrolled back or recorded is unacceptable.
|
|
217
|
+
const redraw = () => { rl.output.write(`${CSI}2K${CSI}200D${question}`); };
|
|
218
|
+
rl.output.write(question);
|
|
219
|
+
rl.input.on('data', redraw);
|
|
220
|
+
rl.question('', (answer) => {
|
|
221
|
+
rl.input.off('data', redraw);
|
|
222
|
+
rl.output.write('\n');
|
|
223
|
+
rl.close();
|
|
224
|
+
resolve(answer);
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function confirm(question, fallback = false) {
|
|
234
|
+
const suffix = fallback ? '[Y/n]' : '[y/N]';
|
|
235
|
+
const answer = (await ask(`${question} ${suffix} `)).toLowerCase();
|
|
236
|
+
if (!answer) return fallback;
|
|
237
|
+
return answer === 'y' || answer === 'yes';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function bytes(value) {
|
|
241
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
242
|
+
let size = value ?? 0;
|
|
243
|
+
let unit = 0;
|
|
244
|
+
while (size >= 1024 && unit < units.length - 1) { size /= 1024; unit += 1; }
|
|
245
|
+
return `${size < 10 && unit > 0 ? size.toFixed(1) : Math.round(size)}${units[unit]}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function compact(value) {
|
|
249
|
+
if (value === null || value === undefined) return '—';
|
|
250
|
+
if (Math.abs(value) >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
|
|
251
|
+
if (Math.abs(value) >= 1000) return `${(value / 1000).toFixed(1)}k`;
|
|
252
|
+
return String(Math.round(value));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function usd(value) {
|
|
256
|
+
if (value === null || value === undefined) return '—';
|
|
257
|
+
return value >= 100 ? `$${Math.round(value)}` : `$${value.toFixed(2)}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function duration(ms) {
|
|
261
|
+
if (!ms || ms < 0) return '0m';
|
|
262
|
+
const minutes = Math.round(ms / 60_000);
|
|
263
|
+
if (minutes < 60) return `${minutes}m`;
|
|
264
|
+
const hours = Math.floor(minutes / 60);
|
|
265
|
+
return minutes % 60 ? `${hours}h${minutes % 60}m` : `${hours}h`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function percent(fraction) {
|
|
269
|
+
return fraction === null || fraction === undefined ? '—' : `${Math.round(fraction * 100)}%`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export { COLOR, UNICODE, CSI };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove every syndes hook, leave everyone else's alone, and ASK before
|
|
3
|
+
* touching the ledger.
|
|
4
|
+
*
|
|
5
|
+
* The default is to keep the data. Uninstalling a tracker is not the same as
|
|
6
|
+
* asking it to destroy your history, and conflating the two is unrecoverable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { rmSync, existsSync } from 'node:fs';
|
|
10
|
+
import { installDir, dataDir, displayPath } from '../runtime/paths.mjs';
|
|
11
|
+
import { uninstallHooks } from './settings.mjs';
|
|
12
|
+
import { height } from '../ledger/read.mjs';
|
|
13
|
+
import { usage } from '../ledger/retention.mjs';
|
|
14
|
+
|
|
15
|
+
export function uninstall({ purge = false } = {}) {
|
|
16
|
+
const steps = [];
|
|
17
|
+
|
|
18
|
+
const { removed, backup } = uninstallHooks();
|
|
19
|
+
steps.push({ step: 'hooks', detail: removed.length ? `removed from ${removed.join(', ')}` : 'none were wired', backup });
|
|
20
|
+
|
|
21
|
+
if (existsSync(installDir)) {
|
|
22
|
+
rmSync(installDir, { recursive: true, force: true });
|
|
23
|
+
steps.push({ step: 'files', detail: displayPath(installDir) });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (purge) {
|
|
27
|
+
const { seq } = height();
|
|
28
|
+
const { bytes } = usage();
|
|
29
|
+
rmSync(dataDir, { recursive: true, force: true });
|
|
30
|
+
steps.push({ step: 'ledger', detail: `deleted ${seq + 1} records (${bytes} bytes)`, destructive: true });
|
|
31
|
+
} else {
|
|
32
|
+
steps.push({ step: 'ledger', detail: `kept at ${displayPath(dataDir)} — use --purge to delete` });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { steps, purged: purge };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** What a purge would destroy, so the confirmation can name it. */
|
|
39
|
+
export function purgeImpact() {
|
|
40
|
+
const { seq, days } = height();
|
|
41
|
+
const { bytes } = usage();
|
|
42
|
+
return { records: seq + 1, days, bytes, path: dataDir };
|
|
43
|
+
}
|