atris 3.12.1 → 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.
- package/README.md +13 -12
- package/bin/atris.js +17 -9
- package/commands/business.js +124 -21
- package/commands/computer.js +145 -6
- package/commands/errors.js +155 -0
- package/commands/proof.js +115 -0
- package/commands/pull.js +4 -2
- package/commands/push.js +6 -0
- package/commands/visualize.js +324 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Then read the workspace's `atris/atris.md` and follow it exactly. `atris.md` is
|
|
|
27
27
|
|
|
28
28
|
| File | Purpose |
|
|
29
29
|
|------|---------|
|
|
30
|
-
| `atris/atris.md` |
|
|
30
|
+
| `atris/atris.md` | Main instructions for agents working in this repo |
|
|
31
31
|
| `atris/MAP.md` | Navigation index with file:line refs |
|
|
32
32
|
| `atris/TODO.md` | Shared task queue |
|
|
33
33
|
| `atris/logs/YYYY/YYYY-MM-DD.md` | Daily log, inbox, notes, completions |
|
|
@@ -67,7 +67,7 @@ atris
|
|
|
67
67
|
|
|
68
68
|
`atris init` scaffolds the workspace, including `atris/wiki/`. `atris` loads context and hands the workflow off to `atris/atris.md`.
|
|
69
69
|
|
|
70
|
-
If you're still shaping the idea, use `atris brainstorm`. If you want Atris to keep cycling, use `atris run` or `atris autopilot`. If you want
|
|
70
|
+
If you're still shaping the idea, use `atris brainstorm`. If you want Atris to keep cycling, use `atris run` or `atris autopilot`. If you want project memory checked for stale pages and missing context, use `atris loop`. `atris activate` surfaces wiki state from `atris/wiki/STATUS.md` when it exists.
|
|
71
71
|
|
|
72
72
|
Core loop: `plan` -> `do` -> `review`
|
|
73
73
|
|
|
@@ -84,9 +84,9 @@ atris business onboard --website https://blondish.world --contact "Joel Zimmerma
|
|
|
84
84
|
atris align --fix
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
That creates the cloud business, writes `.atris/business.json`, initializes `.atris/state/` for events
|
|
87
|
+
That creates the cloud business, writes `.atris/business.json`, initializes `.atris/state/` for events and run history, and scaffolds the local `atris/` workspace under `~/arena/atris-business/<slug>/` with starter roles, a default recap template, and an initial task queue in `atris/TODO.md`.
|
|
88
88
|
|
|
89
|
-
If you do not have a neat source pack yet, `atris business onboard` is the
|
|
89
|
+
If you do not have a neat source pack yet, `atris business onboard` is the easiest intake step: give it a website, a named human, a few notes, or run it in a folder with loose files. Atris turns that into raw intake, a starter brief, a first workflow, a safe next action, and a short operator brief.
|
|
90
90
|
|
|
91
91
|
You can also use bare input:
|
|
92
92
|
|
|
@@ -120,26 +120,27 @@ atris business record atris/reports/2026-04-12-operator-recap.md --outcome mixed
|
|
|
120
120
|
| `atris ingest` | Stage raw evidence into `atris/context/` and compile into `atris/wiki/` |
|
|
121
121
|
| `atris loop` | Refresh wiki health, stale/orphan signals, and next ingest candidates |
|
|
122
122
|
| `atris wiki` | Full wiki namespace: ingest, query, lint, search, log, and loop |
|
|
123
|
-
| `atris
|
|
123
|
+
| `atris receipt` | Save evidence from an agent run |
|
|
124
|
+
| `atris experiments` | Run small experiments and compare results |
|
|
124
125
|
|
|
125
126
|
## Built-In Systems
|
|
126
127
|
|
|
127
128
|
- `atris learn` stores structured project memory in `atris/learnings.jsonl`
|
|
128
129
|
- `atris wiki` keeps repo memory in `atris/wiki/` by default, with `--cloud` when you want the remote workspace path
|
|
129
130
|
- `atris ingest` now stages local source packs under `atris/context/_ingest/`, writes a manifest receipt, and refreshes `atris/wiki/STATUS.md` plus `log.md`
|
|
130
|
-
- `atris wiki --private`
|
|
131
|
+
- `atris wiki --private` stores local-only sensitive notes under `.atris/presidio/`
|
|
131
132
|
- `atris loop` refreshes `atris/wiki/STATUS.md` and `atris/wiki/log.md`, flags stale/orphan pages, and suggests the next ingest
|
|
132
133
|
- `atris activate` loads the current wiki status so the next session starts with project memory, not just tasks
|
|
133
|
-
- `atris experiments` runs
|
|
134
|
+
- `atris experiments` runs small test packs in `atris/experiments/`
|
|
134
135
|
- `atris pull` and `atris push` sync cloud workspaces and journals
|
|
135
136
|
|
|
136
137
|
## Verifiable Feedback Loop
|
|
137
138
|
|
|
138
139
|
Under the hood, Atris can keep score on real repo work.
|
|
139
140
|
|
|
140
|
-
-
|
|
141
|
-
- `atris autopilot` can run that check after review
|
|
142
|
-
- Future
|
|
141
|
+
- Tasks can carry a `Verify:` command, so work can end on a deterministic check instead of pure prose.
|
|
142
|
+
- `atris autopilot` can run that check after review and record the result in the journal.
|
|
143
|
+
- Future task picks can use recent results, so Atris learns from repo-local history without claiming model retraining.
|
|
143
144
|
|
|
144
145
|
## Benchmark Harness
|
|
145
146
|
|
|
@@ -167,7 +168,7 @@ What to inspect:
|
|
|
167
168
|
- receipts land in `atris/experiments/endstate-baseline/artifacts/` and
|
|
168
169
|
`atris/experiments/endstate-stack/artifacts/`
|
|
169
170
|
- scores append to each pack's `results.tsv`
|
|
170
|
-
- `atris experiments compare endstate` prints the latest side-by-side
|
|
171
|
+
- `atris experiments compare endstate` prints the latest side-by-side comparison
|
|
171
172
|
- `atris experiments replay endstate` runs the full public dry-run rehearsal
|
|
172
173
|
- the benchmark contract lives at `atris/features/endstate/contract.md`
|
|
173
174
|
- the verification log lives at `atris/features/endstate/validate.md`
|
|
@@ -198,7 +199,7 @@ For Codex, copy any skill folder into `~/.codex/skills/`.
|
|
|
198
199
|
## v3.2.0
|
|
199
200
|
|
|
200
201
|
- **Staleness gate** — tasks tagged `[unverified]` are skipped at the moment of use, not pruned eagerly. Three-state model: actionable / unverified / deleted.
|
|
201
|
-
- **Lesson gate** — `isLessonResolved` checks whether a lesson already shipped before proposing new
|
|
202
|
+
- **Lesson gate** — `isLessonResolved` checks whether a lesson already shipped before proposing new work from it. Prevents the loop from re-solving solved problems.
|
|
202
203
|
- **`atris release`** — new command: tags the version, bumps package.json, creates a GitHub release, and drafts a `/launch` post in one shot.
|
|
203
204
|
- **Shell injection fix** — `checkStaleness` switched from `execSync` string interpolation to `execFileSync` with args arrays. Markdown-derived content (task titles, inbox items) no longer reaches a shell.
|
|
204
205
|
- **Codex hardening** — `atris activate` and `atris` entry point detect Codex environments and write `AGENTS.md` so Codex sessions start with workspace context.
|
package/bin/atris.js
CHANGED
|
@@ -247,7 +247,7 @@ function showHelp() {
|
|
|
247
247
|
console.log('Optional helpers:');
|
|
248
248
|
console.log(' brainstorm - Explore ideas conversationally before planning');
|
|
249
249
|
console.log(' autopilot - Guided loop that can clarify TODOs and run plan → do → review');
|
|
250
|
-
console.log(' visualize -
|
|
250
|
+
console.log(' visualize - Generate a Slack/deck-ready visual from a prompt');
|
|
251
251
|
console.log('');
|
|
252
252
|
console.log('Experiments:');
|
|
253
253
|
console.log(' experiments init [slug] - Prepare atris/experiments/ or scaffold a pack');
|
|
@@ -272,7 +272,7 @@ function showHelp() {
|
|
|
272
272
|
console.log(' wake [business] - Resume workspace (agents restart)');
|
|
273
273
|
console.log('');
|
|
274
274
|
console.log('Business:');
|
|
275
|
-
console.log(' business init <name> -
|
|
275
|
+
console.log(' business init <name> - RECOMMENDED: create business environment (cloud + local)');
|
|
276
276
|
console.log(' business onboard - Onboard from sparse input (--name, --website, --contact)');
|
|
277
277
|
console.log(' business add <slug> - Connect a business');
|
|
278
278
|
console.log(' business list - Show connected businesses');
|
|
@@ -280,7 +280,7 @@ function showHelp() {
|
|
|
280
280
|
console.log(' business team [slug] - Show members, roles, and admin access');
|
|
281
281
|
console.log(' business health <slug> - Health report (members, workspace, issues)');
|
|
282
282
|
console.log(' business audit - One-line health summary of all businesses');
|
|
283
|
-
console.log(' business create <name> -
|
|
283
|
+
console.log(' business create <name> - Cloud-only business record; add --workspace to also scaffold local');
|
|
284
284
|
console.log(' business connect <svc> - Wire a skill/integration');
|
|
285
285
|
console.log(' business notify <mode> - Set notification mode (digest/silent/push)');
|
|
286
286
|
console.log(' business deploy <slug> - Push local business to cloud');
|
|
@@ -291,6 +291,7 @@ function showHelp() {
|
|
|
291
291
|
console.log('');
|
|
292
292
|
console.log('Cloud & agents:');
|
|
293
293
|
console.log(' computer - Talk directly to the AI computer (bash or agent exec)');
|
|
294
|
+
console.log(' receipt - Save evidence from an agent run');
|
|
294
295
|
console.log(' console - Start/attach always-on coding console (tmux daemon)');
|
|
295
296
|
console.log(' agent - Select which Atris agent to use');
|
|
296
297
|
console.log(' chat - Chat with the selected Atris agent');
|
|
@@ -432,10 +433,10 @@ const { planAtris: planCmd, doAtris: doCmd, reviewAtris: reviewCmd } = require('
|
|
|
432
433
|
// Check if this is a known command or natural language input
|
|
433
434
|
const knownCommands = ['init', 'log', 'status', 'analytics', 'visualize', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
434
435
|
'activate', '_activate', 'agent', 'chat', 'console', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
435
|
-
'clean', 'verify', 'search', 'skill', 'member', 'app', 'learn', 'plugin', 'experiments', 'pull', 'push', 'align', 'terminal', 'computer', 'diff', 'business', 'sync',
|
|
436
|
+
'clean', 'verify', 'search', 'skill', 'member', 'app', 'learn', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'align', 'terminal', 'computer', 'diff', 'business', 'sync',
|
|
436
437
|
'ingest', 'query', 'lint', 'loop',
|
|
437
438
|
'gmail', 'calendar', 'twitter', 'slack', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
438
|
-
'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'wiki', 'code-review', 'cr', 'soul', 'fleet'];
|
|
439
|
+
'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet'];
|
|
439
440
|
|
|
440
441
|
// Check if command is an atris.md spec file - triggers welcome visualization
|
|
441
442
|
function isSpecFile(cmd) {
|
|
@@ -868,10 +869,9 @@ if (command === 'init') {
|
|
|
868
869
|
} else if (command === 'shell-init') {
|
|
869
870
|
require('../commands/auth').shellInit();
|
|
870
871
|
} else if (command === 'visualize') {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
require('../commands/visualize').visualizeAtris();
|
|
872
|
+
require('../commands/visualize').visualizeAtris(process.argv.slice(3))
|
|
873
|
+
.then(() => process.exit(0))
|
|
874
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
875
875
|
} else if (command === 'run') {
|
|
876
876
|
const args = process.argv.slice(3);
|
|
877
877
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -1160,6 +1160,10 @@ if (command === 'init') {
|
|
|
1160
1160
|
const subcommand = process.argv[3];
|
|
1161
1161
|
const args = process.argv.slice(4);
|
|
1162
1162
|
require('../commands/experiments').experimentsCommand(subcommand, ...args);
|
|
1163
|
+
} else if (command === 'receipt' || command === 'proof' || command === 'openclaw') {
|
|
1164
|
+
const subcommand = process.argv[3];
|
|
1165
|
+
const args = process.argv.slice(4);
|
|
1166
|
+
require('../commands/proof').proofCommand(subcommand, ...args);
|
|
1163
1167
|
} else if (command === 'setup') {
|
|
1164
1168
|
require('../commands/setup').setupAtris()
|
|
1165
1169
|
.then(() => process.exit(0))
|
|
@@ -1188,6 +1192,10 @@ if (command === 'init') {
|
|
|
1188
1192
|
require('../commands/feedback').feedbackCommand()
|
|
1189
1193
|
.then(() => process.exit(0))
|
|
1190
1194
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
1195
|
+
} else if (command === 'errors') {
|
|
1196
|
+
require('../commands/errors').errorsCommand()
|
|
1197
|
+
.then(() => process.exit(0))
|
|
1198
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
1191
1199
|
} else {
|
|
1192
1200
|
console.log(`Unknown command: ${command}`);
|
|
1193
1201
|
console.log('Run "atris help" to see available commands');
|
package/commands/business.js
CHANGED
|
@@ -13,6 +13,20 @@ function getBusinessConfigPath() {
|
|
|
13
13
|
return path.join(dir, 'businesses.json');
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function slugify(name) {
|
|
17
|
+
return String(name || '')
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.trim()
|
|
20
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
21
|
+
.replace(/\s+/g, '-')
|
|
22
|
+
.replace(/-+/g, '-')
|
|
23
|
+
.replace(/^-+|-+$/g, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isHelpToken(arg) {
|
|
27
|
+
return arg === '--help' || arg === '-h' || arg === 'help' || arg === '-?';
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
function loadBusinesses() {
|
|
17
31
|
const p = getBusinessConfigPath();
|
|
18
32
|
if (!fs.existsSync(p)) return {};
|
|
@@ -774,7 +788,45 @@ function detectBusinessSlug(explicitSlug) {
|
|
|
774
788
|
}
|
|
775
789
|
}
|
|
776
790
|
|
|
791
|
+
async function findExistingBusinessBySlug(slug, token) {
|
|
792
|
+
if (!slug) return null;
|
|
793
|
+
|
|
794
|
+
// Local cache first — no network round-trip needed.
|
|
795
|
+
const local = loadBusinesses();
|
|
796
|
+
if (local[slug]) {
|
|
797
|
+
return { id: local[slug].business_id, name: local[slug].name, slug, source: 'local' };
|
|
798
|
+
}
|
|
799
|
+
for (const v of Object.values(local)) {
|
|
800
|
+
if (v && v.slug === slug) {
|
|
801
|
+
return { id: v.business_id, name: v.name, slug, source: 'local' };
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (!token) return null;
|
|
806
|
+
|
|
807
|
+
// Cloud lookup — covers businesses the user is a member of but hasn't added.
|
|
808
|
+
const direct = await apiRequestJson(`/business/by-slug/${encodeURIComponent(slug)}`, {
|
|
809
|
+
method: 'GET',
|
|
810
|
+
token,
|
|
811
|
+
});
|
|
812
|
+
if (direct.ok && direct.data && direct.data.id) {
|
|
813
|
+
return { id: direct.data.id, name: direct.data.name, slug: direct.data.slug || slug, source: 'cloud' };
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const list = await apiRequestJson('/business/', { method: 'GET', token });
|
|
817
|
+
if (list.ok && Array.isArray(list.data)) {
|
|
818
|
+
const match = list.data.find(b => b && b.slug === slug);
|
|
819
|
+
if (match) return { id: match.id, name: match.name, slug: match.slug, source: 'cloud' };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
|
|
777
825
|
async function addBusiness(slug) {
|
|
826
|
+
if (!slug || isHelpToken(slug)) {
|
|
827
|
+
console.error('Usage: atris business add <slug>');
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
778
830
|
if (!slug) {
|
|
779
831
|
console.error('Usage: atris business add <slug>');
|
|
780
832
|
process.exit(1);
|
|
@@ -1008,7 +1060,7 @@ function listBusinessesLocal(opts = {}) {
|
|
|
1008
1060
|
}
|
|
1009
1061
|
|
|
1010
1062
|
async function removeBusiness(slug) {
|
|
1011
|
-
if (!slug) {
|
|
1063
|
+
if (!slug || isHelpToken(slug)) {
|
|
1012
1064
|
console.error('Usage: atris business remove <slug>');
|
|
1013
1065
|
process.exit(1);
|
|
1014
1066
|
}
|
|
@@ -1262,8 +1314,11 @@ async function businessAudit() {
|
|
|
1262
1314
|
}
|
|
1263
1315
|
|
|
1264
1316
|
async function createBusinessInternal(name, flags = [], mode = 'auto') {
|
|
1265
|
-
if (!name) {
|
|
1317
|
+
if (!name || isHelpToken(name) || String(name).startsWith('-')) {
|
|
1266
1318
|
console.error('Usage: atris business create <name> [--description "..."] [--workspace] [--here|--root <dir>]');
|
|
1319
|
+
if (name && String(name).startsWith('-') && !isHelpToken(name)) {
|
|
1320
|
+
console.error(`\n Refusing to create a business named "${name}" — looks like a flag, not a name.`);
|
|
1321
|
+
}
|
|
1267
1322
|
process.exit(1);
|
|
1268
1323
|
}
|
|
1269
1324
|
|
|
@@ -1275,6 +1330,29 @@ async function createBusinessInternal(name, flags = [], mode = 'auto') {
|
|
|
1275
1330
|
|
|
1276
1331
|
const options = parseCreateBusinessFlags(flags);
|
|
1277
1332
|
const description = options.description;
|
|
1333
|
+
const force = flags.includes('--force') || flags.includes('--allow-duplicate');
|
|
1334
|
+
|
|
1335
|
+
// Pre-flight: refuse to create a duplicate by slug. The backend will silently
|
|
1336
|
+
// suffix `-1`, `-2`, etc., which produces ghost businesses when users actually
|
|
1337
|
+
// wanted to attach to an existing one. Guide them to `atris pull` instead.
|
|
1338
|
+
if (!force) {
|
|
1339
|
+
const desiredSlug = slugify(name);
|
|
1340
|
+
if (desiredSlug) {
|
|
1341
|
+
const existing = await findExistingBusinessBySlug(desiredSlug, creds.token);
|
|
1342
|
+
if (existing) {
|
|
1343
|
+
console.error(`\nA business with slug "${desiredSlug}" already exists.`);
|
|
1344
|
+
console.error(` Name: ${existing.name || desiredSlug}`);
|
|
1345
|
+
if (existing.id) console.error(` ID: ${existing.id}`);
|
|
1346
|
+
console.error('');
|
|
1347
|
+
console.error('To set up a local workspace for it, run:');
|
|
1348
|
+
console.error(` atris pull ${desiredSlug} # into ./${desiredSlug}`);
|
|
1349
|
+
console.error(` atris pull ${desiredSlug} --into <path> # into a custom path`);
|
|
1350
|
+
console.error('');
|
|
1351
|
+
console.error(`To create a NEW business anyway (will be slugged "${desiredSlug}-1"), pass --force.`);
|
|
1352
|
+
process.exit(1);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1278
1356
|
|
|
1279
1357
|
console.log(`Creating business: ${name}...`);
|
|
1280
1358
|
|
|
@@ -1832,12 +1910,52 @@ async function quickstart() {
|
|
|
1832
1910
|
(get 1 email/day instead of every notification)
|
|
1833
1911
|
|
|
1834
1912
|
Templates: saas, agency, ecommerce, content, restaurant
|
|
1913
|
+
|
|
1914
|
+
Rule of thumb:
|
|
1915
|
+
atris business init "<name>" = cloud + local business computer workspace
|
|
1916
|
+
atris business create "<name>" = cloud-only unless you pass --workspace
|
|
1835
1917
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1836
1918
|
`);
|
|
1837
1919
|
}
|
|
1838
1920
|
|
|
1839
1921
|
|
|
1922
|
+
function printBusinessHelp() {
|
|
1923
|
+
console.log('Usage: atris business <command> [args]');
|
|
1924
|
+
console.log('');
|
|
1925
|
+
console.log(' quickstart ← Start here! 3-command guide');
|
|
1926
|
+
console.log('');
|
|
1927
|
+
console.log(' init <name> RECOMMENDED: create a business environment (cloud + local)');
|
|
1928
|
+
console.log(' workspace <name> Alias for init');
|
|
1929
|
+
console.log(' create <name> Cloud-only business record; add --workspace to also scaffold local');
|
|
1930
|
+
console.log(' add <slug> Register an existing cloud business');
|
|
1931
|
+
console.log(' list Show registered businesses');
|
|
1932
|
+
console.log(' team [slug] Show members, roles, and admin access');
|
|
1933
|
+
console.log(' status <slug> Quick status check');
|
|
1934
|
+
console.log(' health [slug] Full health dashboard');
|
|
1935
|
+
console.log(' audit Audit all businesses');
|
|
1936
|
+
console.log(' connect <service> Connect a skill/integration');
|
|
1937
|
+
console.log(' notify <mode> Set notification mode (digest/silent/push)');
|
|
1938
|
+
console.log(' deploy <slug> Push local business to cloud');
|
|
1939
|
+
console.log(' onboard Seed brief, person, first loop, safe next action, and one-pager from sparse input');
|
|
1940
|
+
console.log(' record <report> Append recap state into events, episodes, and scorecards');
|
|
1941
|
+
console.log(' remove <slug> Unregister locally');
|
|
1942
|
+
console.log('');
|
|
1943
|
+
console.log(' Already-attached business? Run `atris pull <slug>` to scaffold a local workspace.');
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1840
1946
|
async function businessCommand(subcommand, ...args) {
|
|
1947
|
+
// Help intercept — without this, `atris business init --help` would treat
|
|
1948
|
+
// `--help` as a business name and create one. Same for any subcommand that
|
|
1949
|
+
// takes a positional name/slug.
|
|
1950
|
+
if (!subcommand || isHelpToken(subcommand)) {
|
|
1951
|
+
printBusinessHelp();
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
if (args.length > 0 && isHelpToken(args[0])) {
|
|
1955
|
+
printBusinessHelp();
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1841
1959
|
switch (subcommand) {
|
|
1842
1960
|
case 'add':
|
|
1843
1961
|
await addBusiness(args[0]);
|
|
@@ -1907,25 +2025,10 @@ async function businessCommand(subcommand, ...args) {
|
|
|
1907
2025
|
await quickstart();
|
|
1908
2026
|
break;
|
|
1909
2027
|
default:
|
|
1910
|
-
console.
|
|
1911
|
-
console.
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
console.log(' init <name> Create a business environment (cloud + local)');
|
|
1915
|
-
console.log(' workspace <name> Alias for init');
|
|
1916
|
-
console.log(' create <name> Create the cloud business; add --workspace for a local business environment');
|
|
1917
|
-
console.log(' add <slug> Register an existing cloud business');
|
|
1918
|
-
console.log(' list Show registered businesses');
|
|
1919
|
-
console.log(' team [slug] Show members, roles, and admin access');
|
|
1920
|
-
console.log(' status <slug> Quick status check');
|
|
1921
|
-
console.log(' health [slug] Full health dashboard');
|
|
1922
|
-
console.log(' audit Audit all businesses');
|
|
1923
|
-
console.log(' connect <service> Connect a skill/integration');
|
|
1924
|
-
console.log(' notify <mode> Set notification mode (digest/silent/push)');
|
|
1925
|
-
console.log(' deploy <slug> Push local business to cloud');
|
|
1926
|
-
console.log(' onboard Seed brief, person, first loop, safe next action, and one-pager from sparse input');
|
|
1927
|
-
console.log(' record <report> Append recap state into events, episodes, and scorecards');
|
|
1928
|
-
console.log(' remove <slug> Unregister locally');
|
|
2028
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
2029
|
+
console.error('');
|
|
2030
|
+
printBusinessHelp();
|
|
2031
|
+
process.exitCode = 1;
|
|
1929
2032
|
}
|
|
1930
2033
|
}
|
|
1931
2034
|
|
package/commands/computer.js
CHANGED
|
@@ -52,8 +52,40 @@ const KNOWN_CHAT_COMMANDS = new Set([
|
|
|
52
52
|
'/start',
|
|
53
53
|
'/status',
|
|
54
54
|
'/worker',
|
|
55
|
+
'/workflow',
|
|
55
56
|
]);
|
|
56
57
|
|
|
58
|
+
const CODEOPS_WORKFLOW_PROMPT = `
|
|
59
|
+
## Atris CodeOps Workflow
|
|
60
|
+
|
|
61
|
+
You are running inside Atris CodeOps with full computer permissions (permission_mode=bypassPermissions).
|
|
62
|
+
Use those permissions to inspect, edit, test, commit, push, and open PRs when the task calls for it.
|
|
63
|
+
|
|
64
|
+
Do not behave like an open-ended chat.
|
|
65
|
+
Every coding or repo operation must follow the scientific workflow:
|
|
66
|
+
OBSERVE -> HYPOTHESIS -> PLAN -> ACTION -> VALIDATION -> EVIDENCE -> NEXT STATE.
|
|
67
|
+
|
|
68
|
+
For a new coding request, first show a concise PLAN with Files, Checks, Risk, and Merge policy.
|
|
69
|
+
If the user has not clearly approved execution, ask for approval before editing.
|
|
70
|
+
If the user explicitly says to execute, proceed after the concise plan.
|
|
71
|
+
|
|
72
|
+
After work, always report:
|
|
73
|
+
- edited_files
|
|
74
|
+
- commands_run
|
|
75
|
+
- validation_result
|
|
76
|
+
- evidence
|
|
77
|
+
- pr_url if any
|
|
78
|
+
- pr_state
|
|
79
|
+
- merge_state
|
|
80
|
+
- next_task
|
|
81
|
+
|
|
82
|
+
Use one of these next states:
|
|
83
|
+
planned, executing, validated, pr_opened, merge_ready, merge_blocked_checks, merge_blocked_policy, merged, failed, needs_human.
|
|
84
|
+
|
|
85
|
+
Never hide failures.
|
|
86
|
+
A blocked check or missing permission is evidence, not success.
|
|
87
|
+
`.trim();
|
|
88
|
+
|
|
57
89
|
function color(code, value) {
|
|
58
90
|
if (process.env.NO_COLOR || !process.stdout.isTTY) return String(value);
|
|
59
91
|
return `\x1b[${code}m${value}\x1b[0m`;
|
|
@@ -165,6 +197,7 @@ function printCloudHelp() {
|
|
|
165
197
|
console.log(' /start Show the beginner flow again');
|
|
166
198
|
console.log(' /help Show this menu');
|
|
167
199
|
console.log(' /status Show cloud computer status');
|
|
200
|
+
console.log(' /workflow Show the CodeOps workflow contract');
|
|
168
201
|
console.log(' /files [path] List files in the workspace');
|
|
169
202
|
console.log(' /run <cmd> Run shell without the model');
|
|
170
203
|
console.log(' /audit [n] Show recent runs, output, and charges');
|
|
@@ -201,6 +234,48 @@ function printCloudStartPanel(ctx, worker, model, billingLabel, authSummary = nu
|
|
|
201
234
|
console.log(ui.dim('Plain English goes to Atris. Slash commands control the computer.'));
|
|
202
235
|
}
|
|
203
236
|
|
|
237
|
+
function appendSystemPrompt(basePrompt, extraPrompt) {
|
|
238
|
+
if (!extraPrompt) return basePrompt || null;
|
|
239
|
+
if (basePrompt && basePrompt.includes('## Atris CodeOps Workflow')) return basePrompt;
|
|
240
|
+
if (!basePrompt) return extraPrompt;
|
|
241
|
+
return `${String(basePrompt).trim()}\n\n${extraPrompt}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function codeOpsCloudOptions(options = {}) {
|
|
245
|
+
return {
|
|
246
|
+
...options,
|
|
247
|
+
worker: options.worker || 'claude',
|
|
248
|
+
mode: 'codeops',
|
|
249
|
+
systemPrompt: appendSystemPrompt(options.systemPrompt, CODEOPS_WORKFLOW_PROMPT),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary = null) {
|
|
254
|
+
console.log('');
|
|
255
|
+
console.log(ui.bold('Atris CodeOps Computer'));
|
|
256
|
+
console.log(`${ctx.businessName} ${ui.dim('/workspace persists, full permissions enabled')}`);
|
|
257
|
+
console.log(`Lane: ${ui.bold(formatWorkerName(worker))} ${ui.dim(formatCloudSelection({ worker, model }))}`);
|
|
258
|
+
console.log(`Billing: ${billingLabel}`);
|
|
259
|
+
if (authSummary) console.log(`${authSummary.label} ${ui.dim(authSummary.detail)}`);
|
|
260
|
+
console.log(`${ui.green('Workflow locked')} ${ui.dim('observe -> plan -> act -> validate -> evidence -> next')}`);
|
|
261
|
+
console.log('');
|
|
262
|
+
console.log(ui.bold('Start here'));
|
|
263
|
+
console.log(' Type a coding goal in plain English.');
|
|
264
|
+
console.log(' CodeOps will plan first, then execute after approval or explicit proceed language.');
|
|
265
|
+
console.log(' Use /workflow to see the contract, /run for shell, /audit for run history, /exit to leave.');
|
|
266
|
+
console.log('');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function printCodeOpsWorkflowContract() {
|
|
270
|
+
console.log('');
|
|
271
|
+
console.log(ui.bold('CodeOps workflow'));
|
|
272
|
+
console.log(' observe -> hypothesis -> plan -> action -> validation -> evidence -> next state');
|
|
273
|
+
console.log('');
|
|
274
|
+
console.log(' Required final evidence: edited_files, commands_run, validation_result, pr_url, pr_state, merge_state, next_task.');
|
|
275
|
+
console.log(' Allowed states: planned, executing, validated, pr_opened, merge_ready, merge_blocked_checks, merge_blocked_policy, merged, failed, needs_human.');
|
|
276
|
+
console.log(' Full permissions stay on; the workflow contract controls how the computer uses them.');
|
|
277
|
+
}
|
|
278
|
+
|
|
204
279
|
function buildLocalBridgeSystemPrompt(sessionId, localRoot, allowBash) {
|
|
205
280
|
const endpoint = `/api/cli/sessions/${sessionId}/file-op`;
|
|
206
281
|
const bashLine = allowBash
|
|
@@ -1386,6 +1461,8 @@ async function computerExec(token, prompt, ctx = null, options = {}) {
|
|
|
1386
1461
|
workspace_id: ctx.workspaceId,
|
|
1387
1462
|
...(options.worker ? { worker: options.worker } : {}),
|
|
1388
1463
|
...(options.model ? { model: options.model } : {}),
|
|
1464
|
+
...(options.systemPrompt ? { system_prompt: options.systemPrompt } : {}),
|
|
1465
|
+
...(options.allowedTools ? { allowed_tools: options.allowedTools } : {}),
|
|
1389
1466
|
},
|
|
1390
1467
|
timeoutMs: 40000,
|
|
1391
1468
|
});
|
|
@@ -1742,6 +1819,10 @@ async function computerChat(token, ctx, initialOptions = {}) {
|
|
|
1742
1819
|
return;
|
|
1743
1820
|
}
|
|
1744
1821
|
|
|
1822
|
+
const isCodeOps = initialOptions.mode === 'codeops' || ctx.slug === 'atris-codeops';
|
|
1823
|
+
const chatSystemPrompt = isCodeOps
|
|
1824
|
+
? appendSystemPrompt(initialOptions.systemPrompt, CODEOPS_WORKFLOW_PROMPT)
|
|
1825
|
+
: initialOptions.systemPrompt;
|
|
1745
1826
|
let sessionId = `biz-${ctx.businessId.slice(0, 8)}-${Date.now().toString(36)}`;
|
|
1746
1827
|
printCloudWordmark();
|
|
1747
1828
|
const selection = await chooseCloudLane(token, ctx, initialOptions);
|
|
@@ -1758,12 +1839,16 @@ async function computerChat(token, ctx, initialOptions = {}) {
|
|
|
1758
1839
|
return;
|
|
1759
1840
|
}
|
|
1760
1841
|
|
|
1761
|
-
|
|
1842
|
+
if (isCodeOps) {
|
|
1843
|
+
printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary);
|
|
1844
|
+
} else {
|
|
1845
|
+
printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
|
|
1846
|
+
}
|
|
1762
1847
|
|
|
1763
1848
|
const rl = readline.createInterface({
|
|
1764
1849
|
input: process.stdin,
|
|
1765
1850
|
output: process.stdout,
|
|
1766
|
-
prompt: 'cloud> ',
|
|
1851
|
+
prompt: isCodeOps ? 'codeops> ' : 'cloud> ',
|
|
1767
1852
|
});
|
|
1768
1853
|
|
|
1769
1854
|
rl.prompt();
|
|
@@ -1782,7 +1867,11 @@ async function computerChat(token, ctx, initialOptions = {}) {
|
|
|
1782
1867
|
if (line === '/start') {
|
|
1783
1868
|
billingLabel = await describeBillingMode(token, ctx, worker);
|
|
1784
1869
|
authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
|
|
1785
|
-
|
|
1870
|
+
if (isCodeOps) {
|
|
1871
|
+
printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary);
|
|
1872
|
+
} else {
|
|
1873
|
+
printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
|
|
1874
|
+
}
|
|
1786
1875
|
rl.prompt();
|
|
1787
1876
|
continue;
|
|
1788
1877
|
}
|
|
@@ -1796,6 +1885,11 @@ async function computerChat(token, ctx, initialOptions = {}) {
|
|
|
1796
1885
|
rl.prompt();
|
|
1797
1886
|
continue;
|
|
1798
1887
|
}
|
|
1888
|
+
if (line === '/workflow') {
|
|
1889
|
+
printCodeOpsWorkflowContract();
|
|
1890
|
+
rl.prompt();
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1799
1893
|
if (line === '/pwd') {
|
|
1800
1894
|
await computerRun(token, 'pwd', ctx);
|
|
1801
1895
|
rl.prompt();
|
|
@@ -1889,7 +1983,12 @@ async function computerChat(token, ctx, initialOptions = {}) {
|
|
|
1889
1983
|
}
|
|
1890
1984
|
}
|
|
1891
1985
|
|
|
1892
|
-
sessionId = await sendBusinessChat(token, ctx, line, sessionId, false, rl, {
|
|
1986
|
+
sessionId = await sendBusinessChat(token, ctx, line, sessionId, false, rl, {
|
|
1987
|
+
worker,
|
|
1988
|
+
model,
|
|
1989
|
+
systemPrompt: chatSystemPrompt,
|
|
1990
|
+
allowedTools: initialOptions.allowedTools,
|
|
1991
|
+
});
|
|
1893
1992
|
rl.prompt();
|
|
1894
1993
|
}
|
|
1895
1994
|
} catch (error) {
|
|
@@ -2318,7 +2417,7 @@ async function runComputer() {
|
|
|
2318
2417
|
console.log(' local-byo Open LOCAL BYO Claude mode; Anthropic tokens, no cloud audit');
|
|
2319
2418
|
console.log(' --cloud Open CLOUD workspace mode in the bound business workspace');
|
|
2320
2419
|
console.log(' cloud Open CLOUD workspace mode in the bound business workspace');
|
|
2321
|
-
console.log(' codeops Open Atris CodeOps
|
|
2420
|
+
console.log(' codeops Open Atris CodeOps workflow computer if your account has access');
|
|
2322
2421
|
console.log(' --worker Cloud worker override: claude | openai');
|
|
2323
2422
|
console.log(' --model Cloud model override');
|
|
2324
2423
|
console.log(' claude|codex Legacy local console backends');
|
|
@@ -2351,6 +2450,9 @@ async function runComputer() {
|
|
|
2351
2450
|
console.log(' atris computer --cloud --worker openai --model gpt-5.4');
|
|
2352
2451
|
console.log(' atris computer cloud');
|
|
2353
2452
|
console.log(' atris computer codeops');
|
|
2453
|
+
console.log(' atris computer codeops status');
|
|
2454
|
+
console.log(' atris computer codeops run "pwd"');
|
|
2455
|
+
console.log(' atris computer codeops exec "Plan a safe repo fix"');
|
|
2354
2456
|
console.log(' atris computer status');
|
|
2355
2457
|
console.log(' atris computer wake');
|
|
2356
2458
|
console.log(' atris computer run "ls -la /workspace"');
|
|
@@ -2370,7 +2472,44 @@ async function runComputer() {
|
|
|
2370
2472
|
console.error('Ask an Atris CodeOps admin to add you to the atris-codeops business.');
|
|
2371
2473
|
return;
|
|
2372
2474
|
}
|
|
2373
|
-
|
|
2475
|
+
const codeopsOptions = codeOpsCloudOptions(cloudOptions);
|
|
2476
|
+
const codeopsSub = args[1];
|
|
2477
|
+
const codeopsRest = args.slice(2).join(' ');
|
|
2478
|
+
if (!codeopsSub || codeopsSub === 'chat') {
|
|
2479
|
+
await computerChat(token, codeopsCtx, codeopsOptions);
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
switch (codeopsSub) {
|
|
2483
|
+
case '--help':
|
|
2484
|
+
case 'help':
|
|
2485
|
+
console.log('Usage: atris computer codeops [chat|status|wake|sleep|run|grep|ls|cat|exec|audit|workflow]');
|
|
2486
|
+
console.log('');
|
|
2487
|
+
console.log('Examples:');
|
|
2488
|
+
console.log(' atris computer codeops');
|
|
2489
|
+
console.log(' atris computer codeops status');
|
|
2490
|
+
console.log(' atris computer codeops run "pwd && git status --short"');
|
|
2491
|
+
console.log(' atris computer codeops exec "Plan the smallest safe fix, then wait"');
|
|
2492
|
+
return;
|
|
2493
|
+
case 'status': return computerStatus(token, codeopsCtx);
|
|
2494
|
+
case 'wake': return computerWake(token, codeopsCtx);
|
|
2495
|
+
case 'sleep': return computerSleep(token, codeopsCtx);
|
|
2496
|
+
case 'run': return computerRun(token, codeopsRest, codeopsCtx);
|
|
2497
|
+
case 'grep': return computerGrep(token, codeopsRest, codeopsCtx);
|
|
2498
|
+
case 'ls': return computerLs(token, codeopsRest || undefined, codeopsCtx);
|
|
2499
|
+
case 'cat': return computerCat(token, codeopsRest, codeopsCtx);
|
|
2500
|
+
case 'exec': return computerExec(token, codeopsRest, codeopsCtx, codeopsOptions);
|
|
2501
|
+
case 'audit': {
|
|
2502
|
+
const limit = codeopsRest ? Number.parseInt(codeopsRest, 10) : 10;
|
|
2503
|
+
return computerAudit(token, codeopsCtx, Number.isFinite(limit) ? limit : 10);
|
|
2504
|
+
}
|
|
2505
|
+
case 'workflow':
|
|
2506
|
+
printCodeOpsWorkflowContract();
|
|
2507
|
+
return;
|
|
2508
|
+
default:
|
|
2509
|
+
console.error(`Unknown CodeOps subcommand: ${codeopsSub}`);
|
|
2510
|
+
console.log('Run: atris computer codeops help');
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2374
2513
|
return;
|
|
2375
2514
|
}
|
|
2376
2515
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors command for Atris CLI — admin dashboard over atris_error_events.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* atris errors List errors from last 24h, grouped by signature
|
|
6
|
+
* atris errors --hours 72 Widen the window (max 720h / 30d)
|
|
7
|
+
* atris errors --limit 1000 Raise the raw-event cap for grouping
|
|
8
|
+
* atris errors show <id> Full detail (stack trace, message) for one event
|
|
9
|
+
*
|
|
10
|
+
* Requires admin role on the user row.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { loadCredentials } = require('../utils/auth');
|
|
14
|
+
const { apiRequestJson } = require('../utils/api');
|
|
15
|
+
|
|
16
|
+
function getToken() {
|
|
17
|
+
const creds = loadCredentials();
|
|
18
|
+
if (!creds || !creds.token) {
|
|
19
|
+
console.error('Not logged in. Run: atris login');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
return creds.token;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function extractFlag(args, ...names) {
|
|
26
|
+
const remaining = [];
|
|
27
|
+
let value = null;
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
const a = args[i];
|
|
30
|
+
const eq = a.indexOf('=');
|
|
31
|
+
const key = eq >= 0 ? a.slice(0, eq) : a;
|
|
32
|
+
if (names.includes(key)) {
|
|
33
|
+
value = eq >= 0 ? a.slice(eq + 1) : args[++i];
|
|
34
|
+
} else {
|
|
35
|
+
remaining.push(a);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return [value, remaining];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function listErrors(args) {
|
|
42
|
+
const [hoursArg, r1] = extractFlag(args, '--hours', '-H');
|
|
43
|
+
const [limitArg, r2] = extractFlag(r1, '--limit', '-L');
|
|
44
|
+
const hours = hoursArg ? parseInt(hoursArg, 10) : 24;
|
|
45
|
+
const limit = limitArg ? parseInt(limitArg, 10) : 500;
|
|
46
|
+
|
|
47
|
+
const token = getToken();
|
|
48
|
+
const result = await apiRequestJson(`/errors?hours=${hours}&limit=${limit}`, {
|
|
49
|
+
method: 'GET',
|
|
50
|
+
token,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
console.error(`Error: ${result.error || 'Failed to fetch errors'}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const data = result.data || {};
|
|
59
|
+
const groups = data.groups || [];
|
|
60
|
+
|
|
61
|
+
if (groups.length === 0) {
|
|
62
|
+
console.log(`No errors in the last ${hours}h. Clean.`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(
|
|
67
|
+
`Errors — last ${data.window_hours}h — ` +
|
|
68
|
+
`${data.total_events} events across ${data.unique_signatures} signatures\n`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
groups.forEach((g, idx) => {
|
|
72
|
+
const s = g.sample || {};
|
|
73
|
+
const shortId = (s.id || '').substring(0, 8);
|
|
74
|
+
const last = s.created_at ? s.created_at.substring(0, 16).replace('T', ' ') : '';
|
|
75
|
+
const status = s.status_code ? ` [${s.status_code}]` : '';
|
|
76
|
+
const msg = (s.message || '').replace(/\s+/g, ' ').substring(0, 140);
|
|
77
|
+
|
|
78
|
+
console.log(`${idx + 1}. x${g.count} ${g.signature}${status}`);
|
|
79
|
+
if (last) console.log(` last: ${last} UTC latest id: ${shortId}`);
|
|
80
|
+
if (msg) console.log(` "${msg}"`);
|
|
81
|
+
console.log('');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
console.log(
|
|
85
|
+
`Run \`atris errors show <id>\` for full stack trace of a specific event.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function showError(errorId) {
|
|
90
|
+
if (!errorId) {
|
|
91
|
+
console.error('Usage: atris errors show <id>');
|
|
92
|
+
console.error('(id must be a full UUID — get one from `atris errors` output)');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const token = getToken();
|
|
97
|
+
const result = await apiRequestJson(`/errors/${encodeURIComponent(errorId)}`, {
|
|
98
|
+
method: 'GET',
|
|
99
|
+
token,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
console.error(`Error: ${result.error || 'Failed to fetch error'}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const e = result.data || {};
|
|
108
|
+
console.log(`Error ${e.id || errorId}`);
|
|
109
|
+
console.log(` type: ${e.error_type || '?'}`);
|
|
110
|
+
console.log(` method: ${e.request_method || '?'}`);
|
|
111
|
+
console.log(` path: ${e.request_path || '?'}`);
|
|
112
|
+
console.log(` status: ${e.status_code || '?'}`);
|
|
113
|
+
console.log(` when: ${e.created_at || '?'}`);
|
|
114
|
+
console.log(` source: ${e.source || '?'}`);
|
|
115
|
+
if (e.user_id) console.log(` user: ${e.user_id}`);
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log('Message:');
|
|
118
|
+
console.log(' ' + (e.message || '(none)').split('\n').join('\n '));
|
|
119
|
+
console.log('');
|
|
120
|
+
if (e.stack_trace) {
|
|
121
|
+
console.log('Stack trace:');
|
|
122
|
+
console.log(' ' + e.stack_trace.split('\n').join('\n '));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function printHelp() {
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log('Usage:');
|
|
129
|
+
console.log(' atris errors List errors from last 24h, grouped');
|
|
130
|
+
console.log(' atris errors --hours 72 Widen the window (max 720h / 30d)');
|
|
131
|
+
console.log(' atris errors --limit 1000 Raise the raw-event cap');
|
|
132
|
+
console.log(' atris errors show <full-uuid> Full detail for one event');
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log('Admin role required.');
|
|
135
|
+
console.log('');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function errorsCommand() {
|
|
139
|
+
const args = process.argv.slice(3);
|
|
140
|
+
const sub = args[0];
|
|
141
|
+
|
|
142
|
+
if (sub === '--help' || sub === '-h' || sub === 'help') {
|
|
143
|
+
printHelp();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (sub === 'show') {
|
|
148
|
+
await showError(args[1]);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await listErrors(args);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { errorsCommand };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function readBusinessBinding(cwd = process.cwd()) {
|
|
5
|
+
const bindingPath = path.join(cwd, '.atris', 'business.json');
|
|
6
|
+
if (!fs.existsSync(bindingPath)) return null;
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(fs.readFileSync(bindingPath, 'utf8'));
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function slugify(value) {
|
|
15
|
+
return String(value || 'business-workflow')
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
18
|
+
.replace(/^-+|-+$/g, '') || 'business-workflow';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ensureDir(dir) {
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeJson(filePath, value) {
|
|
26
|
+
ensureDir(path.dirname(filePath));
|
|
27
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function doctor(cwd = process.cwd()) {
|
|
31
|
+
const binding = readBusinessBinding(cwd);
|
|
32
|
+
console.log('Receipt check');
|
|
33
|
+
console.log(`business binding: ${binding ? `${binding.name || binding.slug || binding.business_id} ready` : 'missing'}`);
|
|
34
|
+
console.log(`receipt folder: ${fs.existsSync(path.join(cwd, '.atris', 'receipts')) ? 'ready' : 'missing'}`);
|
|
35
|
+
console.log('');
|
|
36
|
+
console.log('Next: atris receipt init business-workflow');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function init(taskSlug = 'business-workflow', cwd = process.cwd()) {
|
|
40
|
+
const binding = readBusinessBinding(cwd);
|
|
41
|
+
if (!binding) {
|
|
42
|
+
console.error('No business binding found. Run: atris business init <name> --here');
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const slug = slugify(taskSlug);
|
|
48
|
+
const root = path.join(cwd, '.atris');
|
|
49
|
+
const taskPath = path.join(root, 'tasks', `${slug}.json`);
|
|
50
|
+
const receiptsDir = path.join(root, 'receipts');
|
|
51
|
+
|
|
52
|
+
ensureDir(receiptsDir);
|
|
53
|
+
fs.writeFileSync(path.join(receiptsDir, '.gitkeep'), '');
|
|
54
|
+
writeJson(taskPath, {
|
|
55
|
+
schema: 'atris.receipt_task.v1',
|
|
56
|
+
slug,
|
|
57
|
+
goal: 'Run one business-computer task and save what happened.',
|
|
58
|
+
workspace: {
|
|
59
|
+
business_id: binding.business_id || binding.id || null,
|
|
60
|
+
workspace_id: binding.workspace_id || null,
|
|
61
|
+
name: binding.name || null,
|
|
62
|
+
slug: binding.slug || null,
|
|
63
|
+
},
|
|
64
|
+
runtime: {
|
|
65
|
+
proof_command: 'atris computer proof',
|
|
66
|
+
replay_command: 'atris experiments replay endstate',
|
|
67
|
+
},
|
|
68
|
+
verify: [
|
|
69
|
+
'atris computer proof',
|
|
70
|
+
'atris experiments replay endstate',
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log(`Receipt task ready: ${slug}`);
|
|
75
|
+
console.log(`Task: ${path.relative(cwd, taskPath)}`);
|
|
76
|
+
console.log(`Receipts: ${path.relative(cwd, receiptsDir)}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function run(args = []) {
|
|
80
|
+
const dryRun = args.includes('--dry-run');
|
|
81
|
+
console.log('Receipt run');
|
|
82
|
+
console.log('1. atris computer proof');
|
|
83
|
+
console.log('2. atris experiments replay endstate');
|
|
84
|
+
if (dryRun) {
|
|
85
|
+
console.log('Dry run only; no receipts written.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
console.log('Run those commands, then save the receipt under .atris/receipts/.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function proofCommand(subcommand = 'doctor', ...args) {
|
|
92
|
+
switch (subcommand || 'doctor') {
|
|
93
|
+
case 'doctor':
|
|
94
|
+
return doctor();
|
|
95
|
+
case 'init':
|
|
96
|
+
return init(args[0] || 'business-workflow');
|
|
97
|
+
case 'proof':
|
|
98
|
+
return run(args);
|
|
99
|
+
case 'help':
|
|
100
|
+
case '--help':
|
|
101
|
+
case '-h':
|
|
102
|
+
console.log('Usage: atris receipt [doctor|init <slug>|run --dry-run]');
|
|
103
|
+
return;
|
|
104
|
+
case 'run':
|
|
105
|
+
return run(args);
|
|
106
|
+
default:
|
|
107
|
+
console.error(`Unknown receipt command: ${subcommand}`);
|
|
108
|
+
console.log('Usage: atris receipt [doctor|init <slug>|run --dry-run]');
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
proofCommand,
|
|
115
|
+
};
|
package/commands/pull.js
CHANGED
|
@@ -703,8 +703,10 @@ async function pullBusiness(slug) {
|
|
|
703
703
|
name: businessName,
|
|
704
704
|
}, null, 2));
|
|
705
705
|
|
|
706
|
-
// Wire skills → .claude/skills/ so they work as slash commands
|
|
707
|
-
|
|
706
|
+
// Wire skills → .claude/skills/ so they work as slash commands.
|
|
707
|
+
// Source of truth is atris/skills/ (vendor-neutral, syncs to cloud).
|
|
708
|
+
// .claude/skills/ is a locally-generated adapter Claude Code reads from.
|
|
709
|
+
const skillsDir = path.join(outputDir, 'atris', 'skills');
|
|
708
710
|
const claudeSkillsDir = path.join(outputDir, '.claude', 'skills');
|
|
709
711
|
|
|
710
712
|
if (fs.existsSync(skillsDir)) {
|
package/commands/push.js
CHANGED
|
@@ -367,6 +367,12 @@ async function pushAtris() {
|
|
|
367
367
|
|
|
368
368
|
if (!result.ok) {
|
|
369
369
|
if (result.status === 403) {
|
|
370
|
+
const detail = result.errorMessage || result.error || (result.data && result.data.detail) || '';
|
|
371
|
+
if (detail && /plan required|business, max, or enterprise/i.test(detail)) {
|
|
372
|
+
console.error(`\n Access denied: ${detail}`);
|
|
373
|
+
await emit('access_denied', { error_detail: detail });
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
370
376
|
// Permission denied — retry with only team/ and journal/ files
|
|
371
377
|
const allowed = filesToPush.filter(f => f.path.startsWith('/team/') || f.path.startsWith('/journal/'));
|
|
372
378
|
skipped = filesToPush.filter(f => !f.path.startsWith('/team/') && !f.path.startsWith('/journal/'));
|
package/commands/visualize.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
};
|