jules-ink 0.0.3 → 0.2.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/assets/fonts/Inter-Bold.ttf +0 -0
- package/assets/fonts/Inter-Medium.ttf +0 -0
- package/assets/fonts/Inter-Regular.ttf +0 -0
- package/assets/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/dist/cli.js +146 -9
- package/dist/example.js +27 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +8 -0
- package/dist/label-generator.js +55 -22
- package/dist/pipeline.d.ts +2 -0
- package/dist/pipeline.js +8 -1
- package/dist/processor.d.ts +1 -1
- package/dist/processor.js +2 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +28 -15
- package/dist/session-stream.d.ts +40 -0
- package/dist/session-stream.js +76 -0
- package/dist/summarizer.js +2 -2
- package/dist/tone-presets.d.ts +1 -0
- package/dist/tone-presets.js +8 -0
- package/dist/tones.d.ts +7 -0
- package/dist/tones.js +46 -0
- package/package.json +40 -11
- package/ui/dist/client/_astro/ApiKeyForm.DLtrDqK2.js +1 -0
- package/ui/dist/client/_astro/SessionListPage.BvVZqYsX.js +1 -0
- package/ui/dist/client/_astro/SessionPage.BVAKjd5H.js +14 -0
- package/ui/dist/client/_astro/SettingsPage.Dxak0E-b.js +1 -0
- package/ui/dist/client/_astro/SetupPage.DYREvJ4u.js +1 -0
- package/ui/dist/client/_astro/TopBar.CC2bbZSl.js +9 -0
- package/ui/dist/client/_astro/client.Dc9Vh3na.js +33 -0
- package/ui/dist/client/_astro/index.CwgqzyKA.css +1 -0
- package/ui/dist/client/_astro/index.DiEladB3.js +9 -0
- package/ui/dist/client/assets/03ba5d9f9e95fc3599acebcbe7e5fb9a.js +83 -0
- package/ui/dist/client/assets/a39f5c6fcbb87b419667ec984d2e579a.css +24 -0
- package/ui/dist/client/assets/c290bebe757d80dba414d37aba776275.css +360 -0
- package/ui/dist/client/assets/logo.svg +1 -0
- package/ui/dist/server/_@astrojs-ssr-adapter.mjs +1 -0
- package/ui/dist/server/_noop-middleware.mjs +3 -0
- package/ui/dist/server/chunks/ApiKeyForm_BOfEh4vS.mjs +78 -0
- package/ui/dist/server/chunks/TopBar_CofSXN19.mjs +183 -0
- package/ui/dist/server/chunks/_@astrojs-ssr-adapter_DPVBl9mS.mjs +4412 -0
- package/ui/dist/server/chunks/api-keys_CDFWFUqM.mjs +32 -0
- package/ui/dist/server/chunks/astro/server_DRWWTbaB.mjs +2826 -0
- package/ui/dist/server/chunks/astro-designed-error-pages_CNOLR5cv.mjs +364 -0
- package/ui/dist/server/chunks/fs-lite_COtHaKzy.mjs +157 -0
- package/ui/dist/server/chunks/index_DPFooF1i.mjs +384 -0
- package/ui/dist/server/chunks/node_CH28SuIq.mjs +1896 -0
- package/ui/dist/server/chunks/remote_B3W5fv4r.mjs +188 -0
- package/ui/dist/server/chunks/session-state_2N6xXgp0.mjs +26 -0
- package/ui/dist/server/chunks/sharp_C1PLyHjw.mjs +101 -0
- package/ui/dist/server/chunks/tones_CcEsTVJR.mjs +492 -0
- package/ui/dist/server/entry.mjs +65 -0
- package/ui/dist/server/manifest_Us9s2mbf.mjs +101 -0
- package/ui/dist/server/noop-entrypoint.mjs +3 -0
- package/ui/dist/server/pages/_image.astro.mjs +2 -0
- package/ui/dist/server/pages/api/keys.astro.mjs +50 -0
- package/ui/dist/server/pages/api/label/image.astro.mjs +47 -0
- package/ui/dist/server/pages/api/print-stack/_id_.astro.mjs +38 -0
- package/ui/dist/server/pages/api/print-stack.astro.mjs +68 -0
- package/ui/dist/server/pages/api/print.astro.mjs +54 -0
- package/ui/dist/server/pages/api/printers.astro.mjs +44 -0
- package/ui/dist/server/pages/api/session/_id_/pause.astro.mjs +29 -0
- package/ui/dist/server/pages/api/session/_id_/stream.astro.mjs +101 -0
- package/ui/dist/server/pages/api/sessions.astro.mjs +49 -0
- package/ui/dist/server/pages/api/tones/_name_.astro.mjs +35 -0
- package/ui/dist/server/pages/api/tones.astro.mjs +49 -0
- package/ui/dist/server/pages/index.astro.mjs +268 -0
- package/ui/dist/server/pages/session.astro.mjs +1214 -0
- package/ui/dist/server/pages/settings.astro.mjs +98 -0
- package/ui/dist/server/renderers.mjs +202 -0
- package/assets/fonts/GoogleSans-Bold.ttf +0 -0
- package/assets/fonts/GoogleSans-Regular.ttf +0 -0
- package/assets/fonts/GoogleSansMono-Regular.ttf +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import {
|
|
3
|
+
import { generateLabel } from './label-generator.js';
|
|
4
|
+
import thermal from './print.js';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
4
8
|
const program = new Command();
|
|
5
9
|
program
|
|
6
10
|
.name('jules-ink')
|
|
@@ -18,24 +22,157 @@ program
|
|
|
18
22
|
const sessionId = options.session;
|
|
19
23
|
const model = options.model;
|
|
20
24
|
const tone = options.tone;
|
|
21
|
-
const
|
|
25
|
+
const printerName = options.printer;
|
|
22
26
|
const outputDir = options.output;
|
|
23
27
|
console.log(`\n🚀 Starting Label Pipeline for Session: ${sessionId}`);
|
|
24
28
|
console.log(`📦 Using model: ${model}`);
|
|
25
29
|
console.log(`🎭 Tone: ${tone}`);
|
|
26
30
|
console.log(`===================================================\n`);
|
|
31
|
+
// --- CLI-only: Printer discovery ---
|
|
32
|
+
const hw = thermal();
|
|
33
|
+
let printer = null;
|
|
34
|
+
if (printerName) {
|
|
35
|
+
const printers = await hw.scan();
|
|
36
|
+
printer = printers.find(p => p.name === printerName) || null;
|
|
37
|
+
if (!printer) {
|
|
38
|
+
console.warn(`⚠️ Printer "${printerName}" not found. Labels will be saved to disk only.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
printer = await hw.find();
|
|
43
|
+
}
|
|
44
|
+
if (printer) {
|
|
45
|
+
console.log(`🖨️ Found printer: ${printer.name} (${printer.stat})`);
|
|
46
|
+
await hw.fix(printer.name);
|
|
47
|
+
}
|
|
48
|
+
else if (!printerName) {
|
|
49
|
+
console.warn('⚠️ No printer found. Labels will be saved to disk only.');
|
|
50
|
+
}
|
|
51
|
+
// --- CLI-only: Output directory ---
|
|
52
|
+
const baseDir = outputDir || process.env.JULES_INK_OUTPUT_DIR || 'output';
|
|
53
|
+
const outDir = path.resolve(baseDir, sessionId);
|
|
54
|
+
if (!fs.existsSync(outDir))
|
|
55
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
56
|
+
let repo = 'unknown/repo';
|
|
57
|
+
const { streamSession } = await import('./session-stream.js');
|
|
27
58
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
59
|
+
for await (const event of streamSession(sessionId, { model, tone })) {
|
|
60
|
+
if (event.type === 'session:info') {
|
|
61
|
+
repo = event.repo;
|
|
62
|
+
console.log(`📦 Repository: ${repo}`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (event.type === 'session:error') {
|
|
66
|
+
console.error(`❌ Error: ${event.error}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (event.type === 'session:complete') {
|
|
70
|
+
console.log(`\n✅ Session ${sessionId} processing complete. ${event.totalActivities} activities.`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (event.type === 'activity:processed') {
|
|
74
|
+
console.log(`Processing Activity ${event.index + 1}: ${event.activityType}`);
|
|
75
|
+
console.log(`> Summary: ${event.summary}`);
|
|
76
|
+
// Generate label image
|
|
77
|
+
const labelData = {
|
|
78
|
+
repo,
|
|
79
|
+
sessionId,
|
|
80
|
+
summary: event.summary,
|
|
81
|
+
files: event.files,
|
|
82
|
+
};
|
|
83
|
+
const buffer = await generateLabel(labelData);
|
|
84
|
+
// Save to disk
|
|
85
|
+
const filename = `${event.index.toString().padStart(3, '0')}_${event.activityType}.png`;
|
|
86
|
+
const filePath = path.join(outDir, filename);
|
|
87
|
+
fs.writeFileSync(filePath, buffer);
|
|
88
|
+
// Print if printer available
|
|
89
|
+
if (printer) {
|
|
90
|
+
try {
|
|
91
|
+
console.log(`🖨️ Sending to ${printer.name}...`);
|
|
92
|
+
await hw.fix(printer.name);
|
|
93
|
+
const jobId = await hw.print(printer.name, buffer, {
|
|
94
|
+
fit: true,
|
|
95
|
+
media: 'w288h432',
|
|
96
|
+
});
|
|
97
|
+
console.log(`✅ Job ID: ${jobId}`);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
console.error(`❌ Print failed:`, err);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
console.log(`✓ [${event.activityType}] Processed`);
|
|
104
|
+
console.log(` └─ Summary: "${event.summary.substring(0, 60)}..."`);
|
|
105
|
+
console.log(` └─ Label: ${filePath}\n`);
|
|
106
|
+
}
|
|
33
107
|
}
|
|
34
|
-
console.log(`✅ Session ${sessionId} processing complete.`);
|
|
35
108
|
}
|
|
36
109
|
catch (error) {
|
|
37
110
|
console.error('\n❌ Fatal Error processing session:', error);
|
|
38
111
|
process.exit(1);
|
|
39
112
|
}
|
|
40
113
|
});
|
|
41
|
-
program
|
|
114
|
+
program
|
|
115
|
+
.command('ui')
|
|
116
|
+
.description('Start the UI and API server')
|
|
117
|
+
.option('--api-port <port>', 'API server port', '3000')
|
|
118
|
+
.option('--ui-port <port>', 'UI server port', '4321')
|
|
119
|
+
.action(async (options) => {
|
|
120
|
+
const apiPort = parseInt(options.apiPort);
|
|
121
|
+
const uiPort = parseInt(options.uiPort);
|
|
122
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
123
|
+
const uiSrcDir = path.join(pkgRoot, 'ui', 'src');
|
|
124
|
+
const uiDistDir = path.join(pkgRoot, 'ui', 'dist');
|
|
125
|
+
const devMode = fs.existsSync(uiSrcDir);
|
|
126
|
+
if (!devMode && !fs.existsSync(uiDistDir)) {
|
|
127
|
+
console.error('Neither ui/src/ nor ui/dist/ found. Is the package installed correctly?');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
// Set JULES_INK_ROOT so Astro API routes can find .env and .jules/
|
|
131
|
+
const rootDir = devMode ? pkgRoot : process.cwd();
|
|
132
|
+
process.env.JULES_INK_ROOT = rootDir;
|
|
133
|
+
// Load .env from the root directory
|
|
134
|
+
const { config } = await import('dotenv');
|
|
135
|
+
config({ path: path.join(rootDir, '.env') });
|
|
136
|
+
// Start API server
|
|
137
|
+
const { serve } = await import('@hono/node-server');
|
|
138
|
+
const { default: app } = await import('./server.js');
|
|
139
|
+
const server = serve({ fetch: app.fetch, port: apiPort }, (info) => {
|
|
140
|
+
console.log(`API server running at http://localhost:${info.port}`);
|
|
141
|
+
});
|
|
142
|
+
if (devMode) {
|
|
143
|
+
// Dev mode: Astro dev server with HMR
|
|
144
|
+
const { dev } = await import('astro');
|
|
145
|
+
const devServer = await dev({ root: path.join(pkgRoot, 'ui'), server: { port: uiPort } });
|
|
146
|
+
const cleanup = async () => {
|
|
147
|
+
await devServer.stop();
|
|
148
|
+
server.close();
|
|
149
|
+
process.exit();
|
|
150
|
+
};
|
|
151
|
+
process.on('SIGINT', cleanup);
|
|
152
|
+
process.on('SIGTERM', cleanup);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// Production mode: serve pre-built Astro output
|
|
156
|
+
// Prevent @astrojs/node from auto-starting its own listener
|
|
157
|
+
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
|
|
158
|
+
const entryPath = path.join(uiDistDir, 'server', 'entry.mjs');
|
|
159
|
+
const { handler } = await import(entryPath);
|
|
160
|
+
const http = await import('node:http');
|
|
161
|
+
const uiServer = http.createServer(handler);
|
|
162
|
+
uiServer.listen(uiPort, () => {
|
|
163
|
+
console.log(`UI server running at http://localhost:${uiPort}`);
|
|
164
|
+
});
|
|
165
|
+
const cleanup = () => {
|
|
166
|
+
uiServer.close();
|
|
167
|
+
server.close();
|
|
168
|
+
process.exit();
|
|
169
|
+
};
|
|
170
|
+
process.on('SIGINT', cleanup);
|
|
171
|
+
process.on('SIGTERM', cleanup);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
// Only parse when run directly as the CLI entry point
|
|
175
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
176
|
+
if (process.argv[1] === __filename || process.argv[1]?.endsWith('/jules-ink')) {
|
|
177
|
+
program.parse();
|
|
178
|
+
}
|
package/dist/example.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
async function main() {
|
|
2
|
+
import thermal from './print.js';
|
|
3
|
+
const hw = thermal();
|
|
4
|
+
const printers = await hw.scan();
|
|
5
|
+
console.log(printers);
|
|
6
|
+
// Logs:
|
|
7
|
+
// [
|
|
8
|
+
// {
|
|
9
|
+
// name: 'Brother_MFC_J1205W',
|
|
10
|
+
// stat: 'disabled since Sun Nov 23 18:51:43 2025 -',
|
|
11
|
+
// usb: true,
|
|
12
|
+
// def: false
|
|
13
|
+
// },
|
|
14
|
+
// {
|
|
15
|
+
// name: 'PM-241-BT',
|
|
16
|
+
// stat: 'is idle. enabled since Mon Nov 10 15:45:15 2025',
|
|
17
|
+
// usb: true,
|
|
18
|
+
// def: true
|
|
19
|
+
// }
|
|
20
|
+
// ]
|
|
21
|
+
// const sessionId = '7058525030495993685';
|
|
22
|
+
// for await (const result of processSessionAndPrint(sessionId)) {
|
|
23
|
+
// console.log(result);
|
|
24
|
+
// }
|
|
25
|
+
}
|
|
26
|
+
main();
|
|
27
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { streamSession } from './session-stream.js';
|
|
2
|
+
export type { SessionEvent, SessionStreamOptions } from './session-stream.js';
|
|
3
|
+
export { SessionSummarizer } from './summarizer.js';
|
|
4
|
+
export type { SummarizerConfig, TonePreset } from './summarizer.js';
|
|
5
|
+
export { analyzeChangeSet } from './analyzer.js';
|
|
6
|
+
export type { FileImpact, ChangeSetSummary } from './types.js';
|
|
7
|
+
export { generateLabel } from './label-generator.js';
|
|
8
|
+
export type { LabelData, FileStat } from './label-generator.js';
|
|
9
|
+
export { default as thermal } from './print.js';
|
|
10
|
+
export type { device, config } from './print.js';
|
|
11
|
+
export { TONE_PRESETS } from './tone-presets.js';
|
|
12
|
+
export { loadTones, saveTone, deleteTone } from './tones.js';
|
|
13
|
+
export type { Tone } from './tones.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Library barrel export — the public API of jules-ink
|
|
2
|
+
export { streamSession } from './session-stream.js';
|
|
3
|
+
export { SessionSummarizer } from './summarizer.js';
|
|
4
|
+
export { analyzeChangeSet } from './analyzer.js';
|
|
5
|
+
export { generateLabel } from './label-generator.js';
|
|
6
|
+
export { default as thermal } from './print.js';
|
|
7
|
+
export { TONE_PRESETS } from './tone-presets.js';
|
|
8
|
+
export { loadTones, saveTone, deleteTone } from './tones.js';
|
package/dist/label-generator.js
CHANGED
|
@@ -4,30 +4,48 @@ import fs from 'fs';
|
|
|
4
4
|
import { truncateMiddle } from './utils.js';
|
|
5
5
|
import { parseMarkdownSegments, calculateWrappedSegments } from './utils.js';
|
|
6
6
|
// --- Path Resolution ---
|
|
7
|
+
// Resolve assets from the project root. We try import.meta.url first (works
|
|
8
|
+
// when running directly from dist/), then fall back to process.cwd() which
|
|
9
|
+
// handles the Vite-bundled SSR case where import.meta.url points at the
|
|
10
|
+
// Astro output chunk instead of the original dist/ file.
|
|
7
11
|
import { fileURLToPath } from 'url';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
+
function resolveAssetsDir() {
|
|
13
|
+
// Try 1: relative to this file (dist/label-generator.js → ../assets)
|
|
14
|
+
try {
|
|
15
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const candidate = path.join(dir, '..', 'assets');
|
|
17
|
+
if (fs.existsSync(path.join(candidate, 'fonts')))
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
catch { /* import.meta.url might not resolve usefully */ }
|
|
21
|
+
// Try 2: relative to cwd (project root → assets)
|
|
22
|
+
const cwdCandidate = path.join(process.cwd(), 'assets');
|
|
23
|
+
if (fs.existsSync(path.join(cwdCandidate, 'fonts')))
|
|
24
|
+
return cwdCandidate;
|
|
25
|
+
// Fallback: return the cwd candidate anyway; registerLocalFont will just skip missing files
|
|
26
|
+
return cwdCandidate;
|
|
27
|
+
}
|
|
28
|
+
const ASSETS_DIR = resolveAssetsDir();
|
|
12
29
|
const FONT_DIR = path.join(ASSETS_DIR, 'fonts');
|
|
13
30
|
function registerLocalFont(filename, family) {
|
|
14
31
|
const filePath = path.join(FONT_DIR, filename);
|
|
15
32
|
if (fs.existsSync(filePath))
|
|
16
33
|
GlobalFonts.registerFromPath(filePath, family);
|
|
17
34
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
35
|
+
// Use unique family names to avoid @napi-rs/canvas font-cache poisoning:
|
|
36
|
+
// once a name resolves to a fallback, it's permanently cached even after
|
|
37
|
+
// the real font is registered. Unique names guarantee first-resolution
|
|
38
|
+
// always finds the registered font.
|
|
39
|
+
registerLocalFont('Inter-Medium.ttf', 'LabelSans');
|
|
40
|
+
registerLocalFont('JetBrainsMono-Regular.ttf', 'LabelMono');
|
|
23
41
|
// --- Configuration ---
|
|
24
42
|
const CONFIG = {
|
|
25
43
|
width: 1200,
|
|
26
44
|
height: 1800,
|
|
27
45
|
padding: 64,
|
|
28
46
|
fonts: {
|
|
29
|
-
header:
|
|
30
|
-
stats:
|
|
47
|
+
header: '36px "LabelMono", monospace',
|
|
48
|
+
stats: '42px "LabelMono", monospace',
|
|
31
49
|
},
|
|
32
50
|
layout: {
|
|
33
51
|
headerY: 120,
|
|
@@ -44,12 +62,30 @@ const CONFIG = {
|
|
|
44
62
|
footerLineHeight: 65
|
|
45
63
|
}
|
|
46
64
|
};
|
|
65
|
+
let templatePromise = null;
|
|
66
|
+
async function getTemplate() {
|
|
67
|
+
if (templatePromise)
|
|
68
|
+
return templatePromise;
|
|
69
|
+
templatePromise = (async () => {
|
|
70
|
+
const templatePath = path.join(ASSETS_DIR, 'template.png');
|
|
71
|
+
if (fs.existsSync(templatePath)) {
|
|
72
|
+
try {
|
|
73
|
+
return await loadImage(templatePath);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error('Failed to load template image:', error);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
})();
|
|
82
|
+
return templatePromise;
|
|
83
|
+
}
|
|
47
84
|
export async function generateLabel(data) {
|
|
48
85
|
const canvas = createCanvas(CONFIG.width, CONFIG.height);
|
|
49
86
|
const ctx = canvas.getContext('2d');
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
const template = await loadImage(templatePath);
|
|
87
|
+
const template = await getTemplate();
|
|
88
|
+
if (template) {
|
|
53
89
|
ctx.drawImage(template, 0, 0, CONFIG.width, CONFIG.height);
|
|
54
90
|
}
|
|
55
91
|
else {
|
|
@@ -80,10 +116,10 @@ function drawHeader(ctx, repo, sessionId) {
|
|
|
80
116
|
const sessionWidth = ctx.measureText(sessionId).width;
|
|
81
117
|
const maxRepoWidth = width - (padding * 2) - sessionWidth - 40;
|
|
82
118
|
let fontSize = 36;
|
|
83
|
-
ctx.font = `${fontSize}px "
|
|
119
|
+
ctx.font = `${fontSize}px "LabelMono", monospace`;
|
|
84
120
|
while (ctx.measureText(repo).width > maxRepoWidth && fontSize > 20) {
|
|
85
121
|
fontSize -= 2;
|
|
86
|
-
ctx.font = `${fontSize}px "
|
|
122
|
+
ctx.font = `${fontSize}px "LabelMono", monospace`;
|
|
87
123
|
}
|
|
88
124
|
ctx.textAlign = 'left';
|
|
89
125
|
ctx.fillText(repo, padding, CONFIG.layout.headerY);
|
|
@@ -99,16 +135,13 @@ function drawBodyAnchored(ctx, text, fixedY, maxHeight) {
|
|
|
99
135
|
let wrappedLines = [];
|
|
100
136
|
let lineHeight = 0;
|
|
101
137
|
let totalTextHeight = 0;
|
|
102
|
-
let weight = 'bold';
|
|
103
138
|
let normalFontStr = '';
|
|
104
|
-
// Monospace font size should match body size roughly
|
|
105
139
|
let codeFontStr = '';
|
|
106
140
|
// 2. Shrink Loop (Now uses segment wrapper)
|
|
107
141
|
do {
|
|
108
|
-
|
|
109
|
-
normalFontStr = `${weight} ${fontSize}px "Google Sans"${EMOJI_FALLBACK}`;
|
|
142
|
+
normalFontStr = `${fontSize}px "LabelSans"`;
|
|
110
143
|
// Use slightly smaller font for mono so it doesn't overpower the text
|
|
111
|
-
codeFontStr =
|
|
144
|
+
codeFontStr = `${fontSize - 4}px "LabelMono", monospace`;
|
|
112
145
|
// Use new wrapper that understands mixed fonts
|
|
113
146
|
wrappedLines = calculateWrappedSegments(ctx, allSegments, maxWidth, normalFontStr, codeFontStr);
|
|
114
147
|
lineHeight = Math.floor(fontSize * lineHeightMultiplier);
|
|
@@ -189,7 +222,7 @@ function drawStatsFixed(ctx, files, fixedY) {
|
|
|
189
222
|
}
|
|
190
223
|
if (hiddenCount > 0) {
|
|
191
224
|
ctx.textAlign = 'center';
|
|
192
|
-
ctx.font = `italic 32px "
|
|
225
|
+
ctx.font = `italic 32px "LabelSans", sans-serif`;
|
|
193
226
|
ctx.fillText(`+ ${hiddenCount} more files...`, CONFIG.width / 2, currentY + 20);
|
|
194
227
|
}
|
|
195
228
|
}
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
/** @deprecated Use `SessionStreamOptions` from `./session-stream.js` instead. */
|
|
1
2
|
export interface ProcessOptions {
|
|
2
3
|
model?: string;
|
|
3
4
|
tone?: string;
|
|
4
5
|
printer?: string;
|
|
5
6
|
outputDir?: string;
|
|
6
7
|
}
|
|
8
|
+
/** @deprecated Use `streamSession` from `./session-stream.js` instead. */
|
|
7
9
|
export declare function processSessionAndPrint(sessionId: string, options?: ProcessOptions): AsyncGenerator<{
|
|
8
10
|
activity: import("@google/jules-sdk").Activity;
|
|
9
11
|
summary: string;
|
package/dist/pipeline.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Use `streamSession` from `./session-stream.js` instead.
|
|
3
|
+
* This module couples streaming, file I/O, and printing in a single generator.
|
|
4
|
+
* The new `streamSession` yields portable typed events without I/O side effects.
|
|
5
|
+
*/
|
|
2
6
|
import { SessionSummarizer } from './summarizer.js';
|
|
3
7
|
import { generateLabel } from './label-generator.js';
|
|
4
8
|
import fs from 'fs';
|
|
5
9
|
import path from 'path';
|
|
6
10
|
import thermal from './print.js';
|
|
11
|
+
/** @deprecated Use `streamSession` from `./session-stream.js` instead. */
|
|
7
12
|
export async function* processSessionAndPrint(sessionId, options = {}) {
|
|
8
13
|
// 1. Initialize Printer Hardware
|
|
9
14
|
const hw = thermal();
|
|
@@ -26,6 +31,8 @@ export async function* processSessionAndPrint(sessionId, options = {}) {
|
|
|
26
31
|
else if (!options.printer) {
|
|
27
32
|
console.warn('⚠️ No printer found. Labels will be saved to disk only.');
|
|
28
33
|
}
|
|
34
|
+
const { connect } = await import('@google/jules-sdk');
|
|
35
|
+
const jules = connect();
|
|
29
36
|
const summarizer = new SessionSummarizer({
|
|
30
37
|
backend: 'cloud',
|
|
31
38
|
apiKey: process.env.GEMINI_API_KEY,
|
package/dist/processor.d.ts
CHANGED
package/dist/processor.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { jules } from '@google/jules-sdk';
|
|
2
1
|
import { analyzeChangeSet } from './analyzer.js';
|
|
3
2
|
/**
|
|
4
3
|
* Streams activities from a Jules session and yields parsed metrics
|
|
@@ -6,6 +5,8 @@ import { analyzeChangeSet } from './analyzer.js';
|
|
|
6
5
|
*/
|
|
7
6
|
export async function* streamChangeMetrics(sessionId, options = {}) {
|
|
8
7
|
const { live = true } = options;
|
|
8
|
+
const { connect } = await import('@google/jules-sdk');
|
|
9
|
+
const jules = connect();
|
|
9
10
|
const session = jules.session(sessionId);
|
|
10
11
|
// Choose between the infinite stream or the finite history
|
|
11
12
|
const activityStream = live ? session.stream() : session.history();
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -3,23 +3,23 @@ import { serve } from '@hono/node-server';
|
|
|
3
3
|
import { cors } from 'hono/cors';
|
|
4
4
|
import { GoogleGenAI } from "@google/genai";
|
|
5
5
|
import thermal from './print.js';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
6
7
|
const app = new Hono();
|
|
7
8
|
const port = 3000;
|
|
8
9
|
const hw = thermal();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
});
|
|
16
|
-
const apiKey = process.env["GEMINI_API_KEY"];
|
|
17
|
-
if (!apiKey) {
|
|
18
|
-
throw new Error("GEMINI_API_KEY environment variable is required");
|
|
19
|
-
}
|
|
20
|
-
const ai = new GoogleGenAI({ apiKey });
|
|
10
|
+
const allowedOrigins = process.env.ALLOWED_ORIGINS
|
|
11
|
+
? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
|
|
12
|
+
: ['http://localhost:4321', 'http://localhost:3000'];
|
|
13
|
+
app.use('/*', cors({
|
|
14
|
+
origin: allowedOrigins,
|
|
15
|
+
}));
|
|
21
16
|
const model = "imagen-4.0-generate-001";
|
|
22
17
|
app.post('/api/generate', async (c) => {
|
|
18
|
+
const apiKey = process.env["GEMINI_API_KEY"];
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
return c.json({ error: "GEMINI_API_KEY environment variable is required" }, 500);
|
|
21
|
+
}
|
|
22
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
23
23
|
const { prompt } = await c.req.json();
|
|
24
24
|
if (!prompt)
|
|
25
25
|
return c.json({ error: 'prompt required' }, 400);
|
|
@@ -52,6 +52,19 @@ app.post('/api/generate', async (c) => {
|
|
|
52
52
|
return c.json({ error: err instanceof Error ? err.message : 'unknown' }, 500);
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
export default app;
|
|
56
|
+
const isMain = process.argv[1] === fileURLToPath(import.meta.url) || process.argv[1]?.endsWith('server.ts');
|
|
57
|
+
if (isMain) {
|
|
58
|
+
if (!process.env["GEMINI_API_KEY"]) {
|
|
59
|
+
throw new Error("GEMINI_API_KEY environment variable is required");
|
|
60
|
+
}
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
hw.watch(controller.signal);
|
|
63
|
+
process.on('SIGINT', () => {
|
|
64
|
+
controller.abort();
|
|
65
|
+
process.exit();
|
|
66
|
+
});
|
|
67
|
+
serve({ fetch: app.fetch, port }, (info) => {
|
|
68
|
+
console.log(`🚀 server at http://localhost:${info.port}`);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FileStat } from './label-generator.js';
|
|
2
|
+
export type SessionEvent = {
|
|
3
|
+
type: 'session:info';
|
|
4
|
+
sessionId: string;
|
|
5
|
+
repo: string;
|
|
6
|
+
title: string;
|
|
7
|
+
state: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: 'activity:processed';
|
|
10
|
+
index: number;
|
|
11
|
+
activityId: string;
|
|
12
|
+
activityType: string;
|
|
13
|
+
summary: string;
|
|
14
|
+
files: FileStat[];
|
|
15
|
+
commitMessage?: string;
|
|
16
|
+
createTime?: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'session:complete';
|
|
19
|
+
sessionId: string;
|
|
20
|
+
totalActivities: number;
|
|
21
|
+
} | {
|
|
22
|
+
type: 'session:error';
|
|
23
|
+
sessionId: string;
|
|
24
|
+
error: string;
|
|
25
|
+
};
|
|
26
|
+
export interface SessionStreamOptions {
|
|
27
|
+
model?: string;
|
|
28
|
+
tone?: string;
|
|
29
|
+
backend?: 'cloud' | 'local';
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
live?: boolean;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
afterIndex?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Portable session streaming core.
|
|
37
|
+
* Yields typed events without any file I/O or printer coupling.
|
|
38
|
+
* Both CLI and Astro SSR consume this same generator.
|
|
39
|
+
*/
|
|
40
|
+
export declare function streamSession(sessionId: string, options?: SessionStreamOptions): AsyncGenerator<SessionEvent>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { SessionSummarizer } from './summarizer.js';
|
|
2
|
+
/**
|
|
3
|
+
* Portable session streaming core.
|
|
4
|
+
* Yields typed events without any file I/O or printer coupling.
|
|
5
|
+
* Both CLI and Astro SSR consume this same generator.
|
|
6
|
+
*/
|
|
7
|
+
export async function* streamSession(sessionId, options = {}) {
|
|
8
|
+
const { model, tone, backend = 'cloud', apiKey, live = true, signal, afterIndex = -1, } = options;
|
|
9
|
+
const summarizer = new SessionSummarizer({
|
|
10
|
+
backend,
|
|
11
|
+
apiKey: apiKey || process.env.GEMINI_API_KEY,
|
|
12
|
+
cloudModelName: model,
|
|
13
|
+
tone,
|
|
14
|
+
});
|
|
15
|
+
const { connect } = await import('@google/jules-sdk');
|
|
16
|
+
const jules = connect();
|
|
17
|
+
let rollingSummary = '';
|
|
18
|
+
const session = jules.session(sessionId);
|
|
19
|
+
// Emit session metadata
|
|
20
|
+
try {
|
|
21
|
+
const sessionInfo = await session.info();
|
|
22
|
+
const repo = sessionInfo.sourceContext?.source?.replace('sources/github/', '') || 'unknown/repo';
|
|
23
|
+
const title = sessionInfo.title || '';
|
|
24
|
+
const state = sessionInfo.state || '';
|
|
25
|
+
yield {
|
|
26
|
+
type: 'session:info',
|
|
27
|
+
sessionId,
|
|
28
|
+
repo,
|
|
29
|
+
title,
|
|
30
|
+
state,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
yield { type: 'session:error', sessionId, error: err.message || String(err) };
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Stream activities
|
|
38
|
+
const activityStream = live ? session.stream() : session.history();
|
|
39
|
+
let count = 0;
|
|
40
|
+
try {
|
|
41
|
+
for await (const activity of activityStream) {
|
|
42
|
+
if (signal?.aborted)
|
|
43
|
+
break;
|
|
44
|
+
// Skip already-processed activities (for resume after pause)
|
|
45
|
+
if (count <= afterIndex) {
|
|
46
|
+
count++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Generate summary
|
|
50
|
+
rollingSummary = await summarizer.generateRollingSummary(rollingSummary, activity);
|
|
51
|
+
// Extract file stats
|
|
52
|
+
const files = summarizer.getLabelData(activity);
|
|
53
|
+
// Extract commit message if available
|
|
54
|
+
const changeSet = activity.artifacts?.find(a => a.type === 'changeSet');
|
|
55
|
+
const commitMessage = changeSet?.gitPatch?.suggestedCommitMessage;
|
|
56
|
+
yield {
|
|
57
|
+
type: 'activity:processed',
|
|
58
|
+
index: count,
|
|
59
|
+
activityId: activity.id,
|
|
60
|
+
activityType: activity.type,
|
|
61
|
+
summary: rollingSummary,
|
|
62
|
+
files,
|
|
63
|
+
commitMessage,
|
|
64
|
+
createTime: activity.createTime,
|
|
65
|
+
};
|
|
66
|
+
count++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
yield { type: 'session:error', sessionId, error: err.message || String(err) };
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!signal?.aborted) {
|
|
74
|
+
yield { type: 'session:complete', sessionId, totalActivities: count };
|
|
75
|
+
}
|
|
76
|
+
}
|
package/dist/summarizer.js
CHANGED
|
@@ -130,7 +130,7 @@ export class SessionSummarizer {
|
|
|
130
130
|
// ... (getLabelData, executeRequest, etc. remain exactly the same) ...
|
|
131
131
|
getLabelData(activity) {
|
|
132
132
|
const changeSetArtifact = activity.artifacts?.find(a => a.type === 'changeSet');
|
|
133
|
-
if (!changeSetArtifact ||
|
|
133
|
+
if (!changeSetArtifact || !changeSetArtifact.gitPatch)
|
|
134
134
|
return [];
|
|
135
135
|
const files = parseDiff(changeSetArtifact.gitPatch.unidiffPatch);
|
|
136
136
|
return files
|
|
@@ -182,7 +182,7 @@ export class SessionSummarizer {
|
|
|
182
182
|
const base = { type: activity.type, originator: activity.originator };
|
|
183
183
|
// 1. Code Changes
|
|
184
184
|
const changeSet = activity.artifacts?.find(a => a.type === 'changeSet');
|
|
185
|
-
if (changeSet && changeSet.
|
|
185
|
+
if (changeSet && changeSet.gitPatch) {
|
|
186
186
|
return {
|
|
187
187
|
...base,
|
|
188
188
|
context: this.buildSmartContext(changeSet.gitPatch.unidiffPatch),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const TONE_PRESETS: readonly ["professional", "pirate", "shakespearean", "excited", "haiku", "noir"];
|
package/dist/tones.d.ts
ADDED