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.
Files changed (73) hide show
  1. package/assets/fonts/Inter-Bold.ttf +0 -0
  2. package/assets/fonts/Inter-Medium.ttf +0 -0
  3. package/assets/fonts/Inter-Regular.ttf +0 -0
  4. package/assets/fonts/JetBrainsMono-Regular.ttf +0 -0
  5. package/dist/cli.js +146 -9
  6. package/dist/example.js +27 -0
  7. package/dist/index.d.ts +13 -0
  8. package/dist/index.js +8 -0
  9. package/dist/label-generator.js +55 -22
  10. package/dist/pipeline.d.ts +2 -0
  11. package/dist/pipeline.js +8 -1
  12. package/dist/processor.d.ts +1 -1
  13. package/dist/processor.js +2 -1
  14. package/dist/server.d.ts +3 -1
  15. package/dist/server.js +28 -15
  16. package/dist/session-stream.d.ts +40 -0
  17. package/dist/session-stream.js +76 -0
  18. package/dist/summarizer.js +2 -2
  19. package/dist/tone-presets.d.ts +1 -0
  20. package/dist/tone-presets.js +8 -0
  21. package/dist/tones.d.ts +7 -0
  22. package/dist/tones.js +46 -0
  23. package/package.json +40 -11
  24. package/ui/dist/client/_astro/ApiKeyForm.DLtrDqK2.js +1 -0
  25. package/ui/dist/client/_astro/SessionListPage.BvVZqYsX.js +1 -0
  26. package/ui/dist/client/_astro/SessionPage.BVAKjd5H.js +14 -0
  27. package/ui/dist/client/_astro/SettingsPage.Dxak0E-b.js +1 -0
  28. package/ui/dist/client/_astro/SetupPage.DYREvJ4u.js +1 -0
  29. package/ui/dist/client/_astro/TopBar.CC2bbZSl.js +9 -0
  30. package/ui/dist/client/_astro/client.Dc9Vh3na.js +33 -0
  31. package/ui/dist/client/_astro/index.CwgqzyKA.css +1 -0
  32. package/ui/dist/client/_astro/index.DiEladB3.js +9 -0
  33. package/ui/dist/client/assets/03ba5d9f9e95fc3599acebcbe7e5fb9a.js +83 -0
  34. package/ui/dist/client/assets/a39f5c6fcbb87b419667ec984d2e579a.css +24 -0
  35. package/ui/dist/client/assets/c290bebe757d80dba414d37aba776275.css +360 -0
  36. package/ui/dist/client/assets/logo.svg +1 -0
  37. package/ui/dist/server/_@astrojs-ssr-adapter.mjs +1 -0
  38. package/ui/dist/server/_noop-middleware.mjs +3 -0
  39. package/ui/dist/server/chunks/ApiKeyForm_BOfEh4vS.mjs +78 -0
  40. package/ui/dist/server/chunks/TopBar_CofSXN19.mjs +183 -0
  41. package/ui/dist/server/chunks/_@astrojs-ssr-adapter_DPVBl9mS.mjs +4412 -0
  42. package/ui/dist/server/chunks/api-keys_CDFWFUqM.mjs +32 -0
  43. package/ui/dist/server/chunks/astro/server_DRWWTbaB.mjs +2826 -0
  44. package/ui/dist/server/chunks/astro-designed-error-pages_CNOLR5cv.mjs +364 -0
  45. package/ui/dist/server/chunks/fs-lite_COtHaKzy.mjs +157 -0
  46. package/ui/dist/server/chunks/index_DPFooF1i.mjs +384 -0
  47. package/ui/dist/server/chunks/node_CH28SuIq.mjs +1896 -0
  48. package/ui/dist/server/chunks/remote_B3W5fv4r.mjs +188 -0
  49. package/ui/dist/server/chunks/session-state_2N6xXgp0.mjs +26 -0
  50. package/ui/dist/server/chunks/sharp_C1PLyHjw.mjs +101 -0
  51. package/ui/dist/server/chunks/tones_CcEsTVJR.mjs +492 -0
  52. package/ui/dist/server/entry.mjs +65 -0
  53. package/ui/dist/server/manifest_Us9s2mbf.mjs +101 -0
  54. package/ui/dist/server/noop-entrypoint.mjs +3 -0
  55. package/ui/dist/server/pages/_image.astro.mjs +2 -0
  56. package/ui/dist/server/pages/api/keys.astro.mjs +50 -0
  57. package/ui/dist/server/pages/api/label/image.astro.mjs +47 -0
  58. package/ui/dist/server/pages/api/print-stack/_id_.astro.mjs +38 -0
  59. package/ui/dist/server/pages/api/print-stack.astro.mjs +68 -0
  60. package/ui/dist/server/pages/api/print.astro.mjs +54 -0
  61. package/ui/dist/server/pages/api/printers.astro.mjs +44 -0
  62. package/ui/dist/server/pages/api/session/_id_/pause.astro.mjs +29 -0
  63. package/ui/dist/server/pages/api/session/_id_/stream.astro.mjs +101 -0
  64. package/ui/dist/server/pages/api/sessions.astro.mjs +49 -0
  65. package/ui/dist/server/pages/api/tones/_name_.astro.mjs +35 -0
  66. package/ui/dist/server/pages/api/tones.astro.mjs +49 -0
  67. package/ui/dist/server/pages/index.astro.mjs +268 -0
  68. package/ui/dist/server/pages/session.astro.mjs +1214 -0
  69. package/ui/dist/server/pages/settings.astro.mjs +98 -0
  70. package/ui/dist/server/renderers.mjs +202 -0
  71. package/assets/fonts/GoogleSans-Bold.ttf +0 -0
  72. package/assets/fonts/GoogleSans-Regular.ttf +0 -0
  73. package/assets/fonts/GoogleSansMono-Regular.ttf +0 -0
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 { processSessionAndPrint } from './pipeline.js';
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 printer = options.printer;
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 generator = processSessionAndPrint(sessionId, { model, tone, printer, outputDir });
29
- for await (const result of generator) {
30
- console.log(`✓ [${result.activity.type}] Processed`);
31
- console.log(` └─ Summary: "${result.summary.substring(0, 60)}..."`);
32
- console.log(` └─ Label: ${result.labelPath}\n`);
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.parse();
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
+ }
@@ -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 {};
@@ -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';
@@ -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
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
- // assets are in ../assets relative to dist/label-generator.js
11
- const ASSETS_DIR = path.join(__dirname, '..', 'assets');
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
- registerLocalFont('GoogleSans-Bold.ttf', 'Google Sans');
19
- registerLocalFont('GoogleSans-Regular.ttf', 'Google Sans');
20
- registerLocalFont('GoogleSansMono-Regular.ttf', 'Google Sans Mono');
21
- // Emoji font fallback (uses system fonts)
22
- const EMOJI_FALLBACK = ', "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji"';
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: `36px "Google Sans Mono", monospace${EMOJI_FALLBACK}`,
30
- stats: `42px "Google Sans Mono", monospace${EMOJI_FALLBACK}`,
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 templatePath = path.join(ASSETS_DIR, 'template.png');
51
- if (fs.existsSync(templatePath)) {
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 "Google Sans Mono", monospace`;
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 "Google Sans Mono", monospace`;
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
- weight = fontSize > 60 ? 'bold' : 'normal';
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 = `normal ${fontSize - 4}px "Google Sans Mono"${EMOJI_FALLBACK}`;
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 "Google Sans", sans-serif`;
225
+ ctx.font = `italic 32px "LabelSans", sans-serif`;
193
226
  ctx.fillText(`+ ${hiddenCount} more files...`, CONFIG.width / 2, currentY + 20);
194
227
  }
195
228
  }
@@ -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
- import { jules } from '@google/jules-sdk';
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,
@@ -1,4 +1,4 @@
1
- import { type Activity } from '@google/jules-sdk';
1
+ import type { Activity } from '@google/jules-sdk';
2
2
  import { ChangeSetSummary } from './types.js';
3
3
  export interface ProcessedActivity {
4
4
  activityId: string;
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
@@ -1 +1,3 @@
1
- export {};
1
+ import { Hono } from 'hono';
2
+ declare const app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
3
+ export default app;
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
- app.use('/*', cors());
10
- const controller = new AbortController();
11
- hw.watch(controller.signal);
12
- process.on('SIGINT', () => {
13
- controller.abort();
14
- process.exit();
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
- serve({ fetch: app.fetch, port }, (info) => {
56
- console.log(`🚀 server at http://localhost:${info.port}`);
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
+ }
@@ -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 || changeSetArtifact.type !== 'changeSet' || !changeSetArtifact.gitPatch)
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.type === 'changeSet' && changeSet.gitPatch) {
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"];
@@ -0,0 +1,8 @@
1
+ export const TONE_PRESETS = [
2
+ 'professional',
3
+ 'pirate',
4
+ 'shakespearean',
5
+ 'excited',
6
+ 'haiku',
7
+ 'noir',
8
+ ];
@@ -0,0 +1,7 @@
1
+ export interface Tone {
2
+ name: string;
3
+ instructions: string;
4
+ }
5
+ export declare function loadTones(): Promise<Tone[]>;
6
+ export declare function saveTone(tone: Tone): Promise<Tone[]>;
7
+ export declare function deleteTone(name: string): Promise<Tone[]>;