atris 3.12.0 → 3.13.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.
@@ -1,20 +1,24 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { spawnSync } = require('child_process');
3
4
  const { getLogPath } = require('../lib/journal');
5
+ const { ensureValidCredentials } = require('../utils/auth');
6
+ const { apiRequestJson } = require('../utils/api');
7
+ const { loadConfig } = require('../utils/config');
4
8
 
5
- function visualizeAtris() {
6
- const { logFile, dateFormatted } = getLogPath();
9
+ const DEFAULT_MODEL = 'gpt-image-2';
10
+ const DEFAULT_SIZE = '1536x1024';
11
+ const DEFAULT_QUALITY = 'high';
12
+
13
+ function legacyVisualizeInbox() {
14
+ const { logFile } = getLogPath();
7
15
 
8
- // Check if log exists
9
16
  if (!fs.existsSync(logFile)) {
10
17
  console.log('✗ No journal entry for today. Run "atris log" to create one.');
11
18
  process.exit(1);
12
19
  }
13
20
 
14
- // Read the log file
15
21
  const logContent = fs.readFileSync(logFile, 'utf8');
16
-
17
- // Extract Inbox section
18
22
  const inboxMatch = logContent.match(/## Inbox\n([\s\S]*?)(?=\n##|$)/);
19
23
  if (!inboxMatch || !inboxMatch[1].trim()) {
20
24
  console.log('✗ No items in Inbox. Add ideas to your journal first.');
@@ -35,7 +39,6 @@ function visualizeAtris() {
35
39
  process.exit(1);
36
40
  }
37
41
 
38
- // Display visualization template
39
42
  console.log('');
40
43
  console.log('┌─────────────────────────────────────────────────────────────┐');
41
44
  console.log('│ Atris Visualize — Break Down & Approval Gate │');
@@ -68,7 +71,320 @@ function visualizeAtris() {
68
71
  console.log('');
69
72
  }
70
73
 
74
+ function parseVisualizeArgs(args = []) {
75
+ const options = {
76
+ model: DEFAULT_MODEL,
77
+ size: DEFAULT_SIZE,
78
+ quality: DEFAULT_QUALITY,
79
+ outputFormat: 'png',
80
+ dryRun: false,
81
+ open: false,
82
+ timeoutMs: 180000,
83
+ };
84
+ const promptParts = [];
85
+
86
+ for (let i = 0; i < args.length; i++) {
87
+ const arg = args[i];
88
+ if (arg === '--') {
89
+ promptParts.push(...args.slice(i + 1));
90
+ break;
91
+ }
92
+ if (arg === '--help' || arg === '-h') options.help = true;
93
+ else if (arg === '--dry-run') options.dryRun = true;
94
+ else if (arg === '--open') options.open = true;
95
+ else if (arg === '--no-open') options.open = false;
96
+ else if (arg === '--raw') options.raw = true;
97
+ else if (arg === '--agent' && args[i + 1]) options.agentId = args[++i];
98
+ else if (arg.startsWith('--agent=')) options.agentId = arg.slice('--agent='.length);
99
+ else if (arg === '--model' && args[i + 1]) options.model = args[++i];
100
+ else if (arg.startsWith('--model=')) options.model = arg.slice('--model='.length);
101
+ else if (arg === '--size' && args[i + 1]) options.size = args[++i];
102
+ else if (arg.startsWith('--size=')) options.size = arg.slice('--size='.length);
103
+ else if (arg === '--quality' && args[i + 1]) options.quality = args[++i];
104
+ else if (arg.startsWith('--quality=')) options.quality = arg.slice('--quality='.length);
105
+ else if (arg === '--out' && args[i + 1]) options.out = args[++i];
106
+ else if (arg.startsWith('--out=')) options.out = arg.slice('--out='.length);
107
+ else if (arg === '--timeout' && args[i + 1]) options.timeoutMs = Number(args[++i]) * 1000;
108
+ else if (arg.startsWith('--timeout=')) options.timeoutMs = Number(arg.slice('--timeout='.length)) * 1000;
109
+ else if (arg === '--format' && args[i + 1]) options.outputFormat = args[++i];
110
+ else if (arg.startsWith('--format=')) options.outputFormat = arg.slice('--format='.length);
111
+ else promptParts.push(arg);
112
+ }
113
+
114
+ return { prompt: promptParts.join(' ').trim(), options };
115
+ }
116
+
117
+ function showVisualizeHelp() {
118
+ console.log('');
119
+ console.log('Usage: atris visualize <prompt> [options]');
120
+ console.log('');
121
+ console.log('Generate a Slack/deck-ready business visual from workspace context.');
122
+ console.log('');
123
+ console.log('Options:');
124
+ console.log(' --model <name> Image model (default: gpt-image-2)');
125
+ console.log(' --size <WxH> Output size (default: 1536x1024)');
126
+ console.log(' --quality <level> Quality (default: high)');
127
+ console.log(' --out <path> Save path (default: atris/reports/visuals/<slug>.png)');
128
+ console.log(' --agent <id> Agent id for backend image endpoint');
129
+ console.log(' --dry-run Print generated prompt without calling the backend');
130
+ console.log(' --open Open the saved PNG after generation');
131
+ console.log(' --raw Send your prompt as-is, without workspace prompt shaping');
132
+ console.log('');
133
+ console.log('No prompt keeps the legacy inbox visualization helper.');
134
+ console.log('');
135
+ }
136
+
137
+ function readTextIfExists(filePath, maxChars) {
138
+ try {
139
+ if (!fs.existsSync(filePath)) return '';
140
+ return fs.readFileSync(filePath, 'utf8').slice(0, maxChars);
141
+ } catch {
142
+ return '';
143
+ }
144
+ }
145
+
146
+ function readBusinessMeta(cwd = process.cwd()) {
147
+ const businessPath = path.join(cwd, '.atris', 'business.json');
148
+ try {
149
+ if (!fs.existsSync(businessPath)) return {};
150
+ return JSON.parse(fs.readFileSync(businessPath, 'utf8'));
151
+ } catch {
152
+ return {};
153
+ }
154
+ }
155
+
156
+ function findRelevantContextFiles(cwd, prompt) {
157
+ const roots = [
158
+ path.join(cwd, 'atris', 'context'),
159
+ path.join(cwd, 'atris', 'wiki'),
160
+ ];
161
+ const words = new Set(
162
+ prompt.toLowerCase().split(/[^a-z0-9]+/).filter(w => w.length >= 4)
163
+ );
164
+ const files = [];
165
+
166
+ function walk(dir) {
167
+ if (!fs.existsSync(dir)) return;
168
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
169
+ const full = path.join(dir, entry.name);
170
+ if (entry.isDirectory()) walk(full);
171
+ else if (entry.isFile() && entry.name.endsWith('.md')) files.push(full);
172
+ }
173
+ }
174
+
175
+ roots.forEach(walk);
176
+ return files
177
+ .map(file => {
178
+ const rel = path.relative(cwd, file);
179
+ const haystack = rel.toLowerCase();
180
+ let score = 0;
181
+ for (const word of words) {
182
+ if (haystack.includes(word)) score += 3;
183
+ }
184
+ const body = readTextIfExists(file, 1200).toLowerCase();
185
+ for (const word of words) {
186
+ if (body.includes(word)) score += 1;
187
+ }
188
+ return { file, rel, score };
189
+ })
190
+ .filter(item => item.score > 0)
191
+ .sort((a, b) => b.score - a.score)
192
+ .slice(0, 4);
193
+ }
194
+
195
+ function collectWorkspaceContext(prompt, cwd = process.cwd()) {
196
+ const business = readBusinessMeta(cwd);
197
+ const chunks = [];
198
+ if (business.name || business.slug) {
199
+ chunks.push(`Workspace: ${business.name || business.slug} (${business.slug || 'no-slug'})`);
200
+ }
201
+
202
+ const mapSnippet = readTextIfExists(path.join(cwd, 'atris', 'MAP.md'), 1400);
203
+ if (mapSnippet) chunks.push(`MAP excerpt:\n${mapSnippet}`);
204
+
205
+ const todoSnippet = readTextIfExists(path.join(cwd, 'atris', 'TODO.md'), 1000);
206
+ if (todoSnippet) chunks.push(`TODO excerpt:\n${todoSnippet}`);
207
+
208
+ const relevant = findRelevantContextFiles(cwd, prompt);
209
+ for (const item of relevant) {
210
+ chunks.push(`${item.rel}:\n${readTextIfExists(item.file, 900)}`);
211
+ }
212
+
213
+ return chunks.join('\n\n---\n\n').slice(0, 6000);
214
+ }
215
+
216
+ function classifyArtifact(prompt) {
217
+ const p = prompt.toLowerCase();
218
+ if (/security|compliance|soc2|soc 2|questionnaire|risk|posture/.test(p)) return 'security posture';
219
+ if (/wbr|weekly|metric|metrics|revenue|p&l|pnl|forecast|dashboard/.test(p)) return 'metric story';
220
+ if (/onboard|setup|connect|workflow|process|flow|steps|how to/.test(p)) return 'workflow';
221
+ if (/architecture|system|infra|stack|api|database|service/.test(p)) return 'architecture';
222
+ if (/compare|comparison|versus|\bvs\b|tradeoff/.test(p)) return 'comparison';
223
+ if (/status|update|recap|progress|roadmap/.test(p)) return 'status update';
224
+ return 'business explainer';
225
+ }
226
+
227
+ function slugify(input) {
228
+ return String(input || 'visual')
229
+ .toLowerCase()
230
+ .replace(/[^a-z0-9]+/g, '-')
231
+ .replace(/^-+|-+$/g, '')
232
+ .slice(0, 54) || 'visual';
233
+ }
234
+
235
+ function defaultOutputPath(prompt, cwd = process.cwd()) {
236
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
237
+ const visualsDir = fs.existsSync(path.join(cwd, 'atris'))
238
+ ? path.join(cwd, 'atris', 'reports', 'visuals')
239
+ : path.join(cwd, 'visuals');
240
+ return path.join(visualsDir, `${slugify(prompt)}-${stamp}.png`);
241
+ }
242
+
243
+ function resolveOutputPath(out, prompt, cwd = process.cwd()) {
244
+ if (!out) return defaultOutputPath(prompt, cwd);
245
+ return path.isAbsolute(out) ? out : path.join(cwd, out);
246
+ }
247
+
248
+ function buildImagePrompt(userPrompt, options = {}, cwd = process.cwd()) {
249
+ if (options.raw) return userPrompt;
250
+
251
+ const artifactType = classifyArtifact(userPrompt);
252
+ const context = collectWorkspaceContext(userPrompt, cwd);
253
+ const contextBlock = context ? `\nWorkspace context to respect:\n${context}\n` : '';
254
+
255
+ return `Use case: productivity-visual
256
+ Asset type: Slack-shareable and deck-ready business artifact
257
+ Artifact type: ${artifactType}
258
+ Primary request: ${userPrompt}
259
+ ${contextBlock}
260
+ Design requirements:
261
+ - Create a polished, modern SaaS-style visual on a clean light background.
262
+ - Use business-appropriate typography, generous spacing, and a restrained palette.
263
+ - Make the visual useful at Slack preview size: large labels, short text, no tiny paragraphs.
264
+ - Prefer a clear structure: flow, comparison, architecture diagram, metric story, or status map depending on the request.
265
+ - If rendering text, keep it concise and accurate; do not invent unsupported names, numbers, claims, or logos.
266
+ - Do not use real third-party logos unless the user explicitly asks.
267
+ - Avoid decorative stock-art scenes. The output should feel like a usable work artifact.
268
+ - Include enough visual hierarchy that a busy operator can understand it in 5 seconds.
269
+ `;
270
+ }
271
+
272
+ async function resolveAgentId(token, explicitAgentId) {
273
+ if (explicitAgentId) return { id: explicitAgentId, label: explicitAgentId };
274
+
275
+ const agentsResult = await apiRequestJson('/agent/my-agents', { method: 'GET', token });
276
+ const agents = agentsResult.data?.my_agents || agentsResult.data?.agents || [];
277
+ const activeAgents = agents.filter(agent => agent.status !== 'inactive' && agent.id);
278
+ const agentById = new Map(activeAgents.map(agent => [agent.id, agent]));
279
+ const fromAccessible = (agentId, fallbackLabel) => {
280
+ if (!agentId || !agentById.has(agentId)) return null;
281
+ const agent = agentById.get(agentId);
282
+ return { id: agent.id, label: agent.name || fallbackLabel || agent.id };
283
+ };
284
+
285
+ const config = loadConfig();
286
+ const configAgent = fromAccessible(config.agent_id, config.agent_name);
287
+ if (configAgent) return configAgent;
288
+
289
+ const business = readBusinessMeta();
290
+ const localBusinessAgent = fromAccessible(business.agent_id, business.agent_name);
291
+ if (localBusinessAgent) return localBusinessAgent;
292
+
293
+ if (business.slug) {
294
+ const list = await apiRequestJson('/business/', { method: 'GET', token });
295
+ const businesses = Array.isArray(list.data) ? list.data : [];
296
+ const match = businesses.find(b => b.slug === business.slug || b.name === business.name);
297
+ const agentId = match?.agent_id || match?.default_agent_id || match?.agent?.id;
298
+ const businessAgent = fromAccessible(agentId, match?.agent_name || match?.agent?.name);
299
+ if (businessAgent) return businessAgent;
300
+ }
301
+
302
+ if (activeAgents.length === 1) return { id: activeAgents[0].id, label: activeAgents[0].name || activeAgents[0].id };
303
+ if (activeAgents.length > 1) return { id: activeAgents[0].id, label: activeAgents[0].name || activeAgents[0].id };
304
+
305
+ throw new Error('No agent found. Run "atris agent" or pass --agent <agent_id>.');
306
+ }
307
+
308
+ function writeImageFile(base64Image, outputPath) {
309
+ const clean = String(base64Image || '').replace(/^data:image\/[a-zA-Z0-9.+-]+;base64,/, '');
310
+ if (!clean) throw new Error('Backend returned no image data.');
311
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
312
+ fs.writeFileSync(outputPath, Buffer.from(clean, 'base64'));
313
+ }
314
+
315
+ function maybeOpenImage(outputPath) {
316
+ if (process.platform === 'darwin') spawnSync('open', [outputPath], { stdio: 'ignore' });
317
+ else if (process.platform === 'win32') spawnSync('cmd', ['/c', 'start', '', outputPath], { stdio: 'ignore' });
318
+ else spawnSync('xdg-open', [outputPath], { stdio: 'ignore' });
319
+ }
320
+
321
+ async function generateVisual(prompt, options = {}) {
322
+ const outputPath = resolveOutputPath(options.out, prompt);
323
+ const imagePrompt = buildImagePrompt(prompt, options);
324
+
325
+ if (options.dryRun) {
326
+ console.log('Atris Visualize dry run');
327
+ console.log(`Model: ${options.model}`);
328
+ console.log(`Size: ${options.size}`);
329
+ console.log(`Output: ${outputPath}`);
330
+ console.log('');
331
+ console.log(imagePrompt.trim());
332
+ return { outputPath, imagePrompt, dryRun: true };
333
+ }
334
+
335
+ const ensured = await ensureValidCredentials(apiRequestJson);
336
+ const creds = ensured.error ? null : ensured.credentials;
337
+ if (!creds?.token) {
338
+ const detail = ensured.detail || ensured.error;
339
+ throw new Error(detail ? `Authentication failed: ${detail}. Run "atris login".` : 'Not logged in. Run "atris login".');
340
+ }
341
+
342
+ const agent = await resolveAgentId(creds.token, options.agentId);
343
+ console.log(`Generating visual with ${options.model} via agent ${agent.label}...`);
344
+
345
+ const result = await apiRequestJson(`/agent/${agent.id}/image/generate`, {
346
+ method: 'POST',
347
+ token: creds.token,
348
+ timeoutMs: options.timeoutMs,
349
+ body: {
350
+ prompt: imagePrompt,
351
+ n: 1,
352
+ size: options.size,
353
+ model: options.model,
354
+ quality: options.quality,
355
+ output_format: options.outputFormat,
356
+ },
357
+ });
358
+
359
+ if (!result.ok) {
360
+ throw new Error(`Image generation failed (${result.status}): ${result.error || result.text || 'unknown error'}`);
361
+ }
362
+
363
+ const image = result.data?.images?.[0];
364
+ writeImageFile(image, outputPath);
365
+ console.log(`Saved: ${outputPath}`);
366
+ if (options.open) maybeOpenImage(outputPath);
367
+ return { outputPath, imagePrompt, model: result.data?.model_used || options.model };
368
+ }
369
+
370
+ async function visualizeAtris(args = process.argv.slice(3)) {
371
+ const { prompt, options } = parseVisualizeArgs(args);
372
+ if (options.help) {
373
+ showVisualizeHelp();
374
+ return;
375
+ }
376
+ if (!prompt) {
377
+ legacyVisualizeInbox();
378
+ return;
379
+ }
380
+ await generateVisual(prompt, options);
381
+ }
71
382
 
72
383
  module.exports = {
73
- visualizeAtris
384
+ visualizeAtris,
385
+ parseVisualizeArgs,
386
+ buildImagePrompt,
387
+ classifyArtifact,
388
+ resolveOutputPath,
389
+ generateVisual,
74
390
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.12.0",
3
+ "version": "3.13.0",
4
4
  "description": "Atris — an operating system for intelligence. Integrates with any agent.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {