atris 3.13.0 → 3.15.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.
@@ -5,6 +5,7 @@
5
5
  * atris computer --cloud — Open CLOUD workspace mode
6
6
  * atris computer wake — Start the computer
7
7
  * atris computer sleep — Stop (files persist)
8
+ * atris computer card — Show the local computer card
8
9
  * atris computer run <command> — Run bash on EC2 (no LLM)
9
10
  * atris computer grep <pattern> — Search files on EC2
10
11
  * atris computer ls [path] — List files
@@ -705,8 +706,8 @@ async function runLocalBridgeOp(token, sessionId, op, timeoutSeconds = 30) {
705
706
  return data;
706
707
  }
707
708
 
708
- function readBusinessBinding() {
709
- const bindingPath = path.join(process.cwd(), '.atris', 'business.json');
709
+ function readBusinessBinding(cwd = process.cwd()) {
710
+ const bindingPath = path.join(cwd, '.atris', 'business.json');
710
711
  if (!fs.existsSync(bindingPath)) return null;
711
712
  try {
712
713
  return JSON.parse(fs.readFileSync(bindingPath, 'utf8'));
@@ -715,6 +716,160 @@ function readBusinessBinding() {
715
716
  }
716
717
  }
717
718
 
719
+ function readPackageMeta(cwd = process.cwd()) {
720
+ const packagePath = path.join(cwd, 'package.json');
721
+ if (!fs.existsSync(packagePath)) return null;
722
+ try {
723
+ return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
724
+ } catch {
725
+ return null;
726
+ }
727
+ }
728
+
729
+ function relIfExists(cwd, target) {
730
+ return fs.existsSync(path.join(cwd, target)) ? target : null;
731
+ }
732
+
733
+ function detectValidationCommand(cwd = process.cwd(), pkg = null) {
734
+ const meta = pkg || readPackageMeta(cwd);
735
+ const testScript = meta?.scripts?.test;
736
+ if (testScript && !/no test specified/i.test(testScript)) return 'npm test';
737
+ if (fs.existsSync(path.join(cwd, 'pytest.ini')) || fs.existsSync(path.join(cwd, 'pyproject.toml'))) return 'pytest';
738
+ if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) return 'cargo test';
739
+ if (fs.existsSync(path.join(cwd, 'go.mod'))) return 'go test ./...';
740
+ return 'none detected';
741
+ }
742
+
743
+ function detectComputerType(cwd = process.cwd(), pkg = null, binding = null) {
744
+ if (binding?.computer_type) return binding.computer_type;
745
+ if (binding?.workspace_type) return binding.workspace_type;
746
+ const meta = pkg || readPackageMeta(cwd);
747
+ if (meta?.bin || fs.existsSync(path.join(cwd, 'bin')) || fs.existsSync(path.join(cwd, 'commands'))) return 'codeops';
748
+ if (fs.existsSync(path.join(cwd, 'docs')) || fs.existsSync(path.join(cwd, 'atris', 'wiki'))) return 'research';
749
+ return 'workspace';
750
+ }
751
+
752
+ function buildComputerCard(cwd = process.cwd()) {
753
+ const binding = readBusinessBinding(cwd);
754
+ const pkg = readPackageMeta(cwd);
755
+ const folderName = path.basename(cwd);
756
+ const ownerName = binding?.name || pkg?.name || folderName;
757
+ const ownerType = binding ? 'business' : 'project';
758
+ const computerName = binding?.computer_name || binding?.workspace_name || `${ownerName} computer`;
759
+ const computerType = detectComputerType(cwd, pkg, binding);
760
+ const memory = [
761
+ relIfExists(cwd, 'atris/MAP.md'),
762
+ relIfExists(cwd, 'atris/TODO.md'),
763
+ relIfExists(cwd, 'atris/wiki'),
764
+ relIfExists(cwd, 'atris/logs'),
765
+ ].filter(Boolean);
766
+ const artifacts = [
767
+ fs.existsSync(path.join(cwd, 'atris')) ? 'atris/reports/' : null,
768
+ relIfExists(cwd, '.atris/receipts'),
769
+ ].filter(Boolean);
770
+
771
+ return {
772
+ ownerName,
773
+ ownerType,
774
+ computerName,
775
+ computerType,
776
+ workspace: cwd,
777
+ loop: 'plan -> do -> review',
778
+ memory,
779
+ validation: detectValidationCommand(cwd, pkg),
780
+ proof: binding ? 'atris computer proof' : 'atris proof run',
781
+ visual: 'atris visualize "<prompt>"',
782
+ artifacts,
783
+ generatedAt: new Date().toISOString(),
784
+ };
785
+ }
786
+
787
+ function renderList(items) {
788
+ return items.length ? items.join(', ') : 'none detected';
789
+ }
790
+
791
+ function renderComputerCard(card) {
792
+ return [
793
+ 'Atris Computer Card',
794
+ '',
795
+ ` Owner: ${card.ownerName} (${card.ownerType})`,
796
+ ` Computer: ${card.computerName}`,
797
+ ` Type: ${card.computerType}`,
798
+ ` Workspace: ${card.workspace}`,
799
+ ` Loop: ${card.loop}`,
800
+ ` Memory: ${renderList(card.memory)}`,
801
+ ` Validate: ${card.validation}`,
802
+ ` Proof: ${card.proof}`,
803
+ ` Visual: ${card.visual}`,
804
+ ` Artifacts: ${renderList(card.artifacts)}`,
805
+ ].join('\n');
806
+ }
807
+
808
+ function renderComputerCardMarkdown(card) {
809
+ return [
810
+ '# Atris Computer Card',
811
+ '',
812
+ `Generated: ${card.generatedAt}`,
813
+ '',
814
+ `- Owner: ${card.ownerName} (${card.ownerType})`,
815
+ `- Computer: ${card.computerName}`,
816
+ `- Type: ${card.computerType}`,
817
+ `- Workspace: ${card.workspace}`,
818
+ `- Loop: ${card.loop}`,
819
+ `- Memory: ${renderList(card.memory)}`,
820
+ `- Validate: ${card.validation}`,
821
+ `- Proof: ${card.proof}`,
822
+ `- Visual: ${card.visual}`,
823
+ `- Artifacts: ${renderList(card.artifacts)}`,
824
+ '',
825
+ ].join('\n');
826
+ }
827
+
828
+ function parseComputerCardArgs(args = []) {
829
+ const options = { write: false, out: null, help: false };
830
+ for (let i = 0; i < args.length; i++) {
831
+ const arg = args[i];
832
+ if (arg === '--help' || arg === '-h') options.help = true;
833
+ else if (arg === '--write') options.write = true;
834
+ else if (arg === '--out' && args[i + 1]) options.out = args[++i];
835
+ else if (arg.startsWith('--out=')) options.out = arg.slice('--out='.length);
836
+ }
837
+ return options;
838
+ }
839
+
840
+ function defaultComputerCardPath(cwd = process.cwd()) {
841
+ if (fs.existsSync(path.join(cwd, 'atris'))) {
842
+ return path.join(cwd, 'atris', 'reports', 'computer-card.md');
843
+ }
844
+ return path.join(cwd, 'computer-card.md');
845
+ }
846
+
847
+ function computerCard(args = [], cwd = process.cwd()) {
848
+ const options = parseComputerCardArgs(args);
849
+ if (options.help) {
850
+ console.log('Usage: atris computer card [--write] [--out <path>]');
851
+ console.log('');
852
+ console.log('Show the local owner/computer card for this workspace.');
853
+ return null;
854
+ }
855
+
856
+ const card = buildComputerCard(cwd);
857
+ console.log(renderComputerCard(card));
858
+
859
+ if (options.write || options.out) {
860
+ const outputPath = options.out
861
+ ? (path.isAbsolute(options.out) ? options.out : path.join(cwd, options.out))
862
+ : defaultComputerCardPath(cwd);
863
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
864
+ fs.writeFileSync(outputPath, renderComputerCardMarkdown(card), 'utf8');
865
+ console.log('');
866
+ console.log(`Wrote ${path.relative(cwd, outputPath) || outputPath}`);
867
+ return outputPath;
868
+ }
869
+
870
+ return card;
871
+ }
872
+
718
873
  async function resolveBusinessContext(token) {
719
874
  const binding = readBusinessBinding();
720
875
  if (!binding) return null;
@@ -2397,6 +2552,11 @@ async function runComputer() {
2397
2552
  return;
2398
2553
  }
2399
2554
 
2555
+ if (sub === 'card') {
2556
+ computerCard(args.slice(1));
2557
+ return;
2558
+ }
2559
+
2400
2560
  if (sub === 'claude' || sub === 'codex') {
2401
2561
  computerLocalLegacy(args);
2402
2562
  return;
@@ -2405,6 +2565,15 @@ async function runComputer() {
2405
2565
  if (sub === '--help') {
2406
2566
  console.log('Usage: atris computer [mode|command]');
2407
2567
  console.log('');
2568
+ console.log('Atris computers are persistent AI workspaces for scoped jobs.');
2569
+ console.log('');
2570
+ console.log(' Owner = User | Business');
2571
+ console.log(' Owner has many Computers');
2572
+ console.log(' Computer = workspace + files + tools + secrets + memory + agents + validation');
2573
+ console.log('');
2574
+ console.log('Common types: codeops, research, CRM, reporting, recruiting, event ops, support, business ops.');
2575
+ console.log('A business can be a company, lab, collective, community, artist, team, or project.');
2576
+ console.log('');
2408
2577
  console.log('First use:');
2409
2578
  console.log(' cd ~/arena/atris-business/<business>');
2410
2579
  console.log(' atris computer');
@@ -2412,6 +2581,7 @@ async function runComputer() {
2412
2581
  console.log('');
2413
2582
  console.log('Modes:');
2414
2583
  console.log(' (default) Choose CLOUD vs LOCAL when both are available');
2584
+ console.log(' card Show the local owner/computer card, no login required');
2415
2585
  console.log(' local Open LOCAL Atris mode; cloud brain edits this folder');
2416
2586
  console.log(' proof Run the local-edit + cloud-isolation + audit proof');
2417
2587
  console.log(' local-byo Open LOCAL BYO Claude mode; Anthropic tokens, no cloud audit');
@@ -2443,6 +2613,8 @@ async function runComputer() {
2443
2613
  console.log('');
2444
2614
  console.log('Examples:');
2445
2615
  console.log(' atris computer');
2616
+ console.log(' atris computer card --write');
2617
+ console.log(' atris business init "My Lab" # shared owner + first/default computer');
2446
2618
  console.log(' atris computer proof');
2447
2619
  console.log(' atris computer local');
2448
2620
  console.log(' atris computer codex');
@@ -2522,6 +2694,7 @@ async function runComputer() {
2522
2694
 
2523
2695
  switch (sub) {
2524
2696
  case 'chat': return computerChat(token, ctx, cloudOptions);
2697
+ case 'card': return computerCard(args.slice(1));
2525
2698
  case 'proof': return computerProof(token, ctx, cloudOptions);
2526
2699
  case 'status': return computerStatus(token, ctx);
2527
2700
  case 'wake': return computerWake(token, ctx);
@@ -2544,4 +2717,9 @@ async function runComputer() {
2544
2717
  }
2545
2718
  }
2546
2719
 
2547
- module.exports = { runComputer };
2720
+ module.exports = {
2721
+ runComputer,
2722
+ buildComputerCard,
2723
+ renderComputerCard,
2724
+ renderComputerCardMarkdown,
2725
+ };
@@ -0,0 +1,289 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawn } = require('child_process');
4
+
5
+ const DEFAULT_INTERVAL_SEC = 120;
6
+ const DEFAULT_DEBOUNCE_SEC = 30;
7
+ const DEFAULT_TIMEOUT_SEC = 600;
8
+
9
+ const IGNORED_DIRS = new Set(['.git', 'node_modules', 'dist', 'build', 'release', '.next', '__pycache__']);
10
+ const IGNORED_FILES = new Set(['.DS_Store']);
11
+
12
+ function parseNumberFlag(args, name, fallback) {
13
+ const eq = args.find((arg) => arg.startsWith(`--${name}=`));
14
+ if (eq) {
15
+ const value = Number(eq.slice(name.length + 3));
16
+ return Number.isFinite(value) && value > 0 ? value : fallback;
17
+ }
18
+ const idx = args.indexOf(`--${name}`);
19
+ if (idx !== -1 && args[idx + 1]) {
20
+ const value = Number(args[idx + 1]);
21
+ return Number.isFinite(value) && value > 0 ? value : fallback;
22
+ }
23
+ return fallback;
24
+ }
25
+
26
+ function parseStringFlag(args, name) {
27
+ const eq = args.find((arg) => arg.startsWith(`--${name}=`));
28
+ if (eq) return eq.slice(name.length + 3);
29
+ const idx = args.indexOf(`--${name}`);
30
+ if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith('-')) return args[idx + 1];
31
+ return null;
32
+ }
33
+
34
+ function firstPositionalArg(args) {
35
+ const flagsWithValues = new Set(['--interval', '--debounce', '--timeout', '--only', '--root']);
36
+ for (let i = 0; i < args.length; i++) {
37
+ const arg = args[i];
38
+ if (flagsWithValues.has(arg)) {
39
+ i += 1;
40
+ continue;
41
+ }
42
+ if (arg.startsWith('-')) continue;
43
+ return arg;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ function readBusinessSlugFromCwd(cwd) {
49
+ const file = path.join(cwd, '.atris', 'business.json');
50
+ if (!fs.existsSync(file)) return null;
51
+ try {
52
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
53
+ return data.slug || data.canonical_slug || data.name || null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function parseLiveOptions(args, cwd = process.cwd()) {
60
+ const first = firstPositionalArg(args);
61
+ return {
62
+ slug: first || readBusinessSlugFromCwd(cwd),
63
+ once: args.includes('--once'),
64
+ dryRun: args.includes('--dry-run'),
65
+ noDoctor: args.includes('--no-doctor'),
66
+ noPush: args.includes('--no-push'),
67
+ intervalSec: parseNumberFlag(args, 'interval', DEFAULT_INTERVAL_SEC),
68
+ debounceSec: parseNumberFlag(args, 'debounce', DEFAULT_DEBOUNCE_SEC),
69
+ timeoutSec: parseNumberFlag(args, 'timeout', DEFAULT_TIMEOUT_SEC),
70
+ only: parseStringFlag(args, 'only'),
71
+ root: parseStringFlag(args, 'root') || path.dirname(cwd),
72
+ cwd,
73
+ };
74
+ }
75
+
76
+ function printLiveHelp() {
77
+ console.log('Usage: atris live [business] [options]');
78
+ console.log('');
79
+ console.log('Keeps a business brain fresh: doctor, pull, watch local changes, push after quiet, and periodically pull.');
80
+ console.log('');
81
+ console.log('Examples:');
82
+ console.log(' atris live atris-labs');
83
+ console.log(' atris live --once');
84
+ console.log(' atris live atris-labs --dry-run');
85
+ console.log(' atris live atris-labs --interval=120 --debounce=30');
86
+ console.log('');
87
+ console.log('Options:');
88
+ console.log(' --once Run one freshness cycle and exit');
89
+ console.log(' --dry-run Print the plan without running pull/push');
90
+ console.log(' --interval <sec> Seconds between cloud pulls (default: 120)');
91
+ console.log(' --debounce <sec> Quiet seconds before pushing local changes (default: 30)');
92
+ console.log(' --timeout <sec> Pull timeout passed through to atris pull (default: 600)');
93
+ console.log(' --only <prefix> Limit pull/push to a path prefix');
94
+ console.log(' --no-doctor Skip business doctor --fix');
95
+ console.log(' --no-push Pull/watch only; never push');
96
+ }
97
+
98
+ function shouldIgnore(relativePath) {
99
+ if (!relativePath) return true;
100
+ const parts = relativePath.split(path.sep);
101
+ if (parts.some((part) => IGNORED_DIRS.has(part))) return true;
102
+ if (IGNORED_FILES.has(path.basename(relativePath))) return true;
103
+ if (relativePath.startsWith(path.join('.atris', 'state'))) return true;
104
+ return false;
105
+ }
106
+
107
+ function collectSnapshot(root) {
108
+ const snapshot = new Map();
109
+
110
+ function walk(dir) {
111
+ let entries;
112
+ try {
113
+ entries = fs.readdirSync(dir, { withFileTypes: true });
114
+ } catch {
115
+ return;
116
+ }
117
+
118
+ for (const entry of entries) {
119
+ const full = path.join(dir, entry.name);
120
+ const rel = path.relative(root, full);
121
+ if (shouldIgnore(rel)) continue;
122
+ if (entry.isDirectory()) {
123
+ walk(full);
124
+ } else if (entry.isFile()) {
125
+ try {
126
+ const stat = fs.statSync(full);
127
+ snapshot.set(rel, `${stat.size}:${Math.floor(stat.mtimeMs)}`);
128
+ } catch {
129
+ // Files can disappear while the operator is saving.
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ walk(root);
136
+ return snapshot;
137
+ }
138
+
139
+ function snapshotsDiffer(a, b) {
140
+ if (a.size !== b.size) return true;
141
+ for (const [key, value] of a.entries()) {
142
+ if (b.get(key) !== value) return true;
143
+ }
144
+ return false;
145
+ }
146
+
147
+ function commandLine(argv) {
148
+ return ['atris', ...argv].join(' ');
149
+ }
150
+
151
+ function runCli(argv, options) {
152
+ if (options.dryRun) {
153
+ console.log(` dry-run: ${commandLine(argv)}`);
154
+ return Promise.resolve({ status: 0 });
155
+ }
156
+
157
+ return new Promise((resolve, reject) => {
158
+ const child = spawn(process.execPath, [path.join(__dirname, '..', 'bin', 'atris.js'), ...argv], {
159
+ cwd: options.cwd,
160
+ stdio: 'inherit',
161
+ env: { ...process.env, ATRIS_SKIP_UPDATE_CHECK: '1' },
162
+ });
163
+ child.on('error', reject);
164
+ child.on('exit', (status) => {
165
+ if (status === 0) resolve({ status });
166
+ else reject(new Error(`${commandLine(argv)} exited ${status}`));
167
+ });
168
+ });
169
+ }
170
+
171
+ function buildPullArgs(options) {
172
+ const args = ['pull', options.slug, '--timeout', String(options.timeoutSec)];
173
+ if (options.only) args.push('--only', options.only);
174
+ return args;
175
+ }
176
+
177
+ function buildPushArgs(options) {
178
+ const args = ['push', options.slug, '--from', options.cwd];
179
+ if (options.only) args.push('--only', options.only);
180
+ return args;
181
+ }
182
+
183
+ async function runFreshnessCycle(options, reason) {
184
+ console.log('');
185
+ console.log(`atris live: ${reason}`);
186
+ if (!options.noDoctor) {
187
+ await runCli(['business', 'doctor', '--fix', '--root', options.root], options);
188
+ }
189
+ if (!options.noPush) {
190
+ await runCli(buildPushArgs(options), options);
191
+ }
192
+ await runCli(buildPullArgs(options), options);
193
+ }
194
+
195
+ async function liveCommand(args = process.argv.slice(3)) {
196
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
197
+ printLiveHelp();
198
+ return;
199
+ }
200
+
201
+ const options = parseLiveOptions(args);
202
+ if (!options.slug) {
203
+ console.error('Usage: atris live [business]');
204
+ console.error('Run inside a business workspace or pass a business slug.');
205
+ process.exit(1);
206
+ }
207
+
208
+ console.log('');
209
+ console.log(`Atris Live: ${options.slug}`);
210
+ console.log(` workspace: ${options.cwd}`);
211
+ console.log(` interval: ${options.intervalSec}s`);
212
+ console.log(` debounce: ${options.debounceSec}s`);
213
+ console.log(` push: ${options.noPush ? 'off' : 'on'}`);
214
+ if (options.only) console.log(` only: ${options.only}`);
215
+
216
+ if (options.dryRun) {
217
+ await runFreshnessCycle(options, 'planned startup cycle');
218
+ if (!options.once) console.log(` dry-run: would watch ${options.cwd} and sync every ${options.intervalSec}s`);
219
+ return;
220
+ }
221
+
222
+ await runFreshnessCycle(options, 'startup freshness cycle');
223
+ if (options.once) return;
224
+
225
+ console.log('');
226
+ console.log('Brain fresh. Watching for local changes. Press Ctrl+C to stop.');
227
+
228
+ let lastSnapshot = collectSnapshot(options.cwd);
229
+ let pendingPush = false;
230
+ let quietTicks = 0;
231
+ let running = false;
232
+
233
+ async function guarded(label, fn) {
234
+ if (running) return;
235
+ running = true;
236
+ try {
237
+ await fn();
238
+ lastSnapshot = collectSnapshot(options.cwd);
239
+ pendingPush = false;
240
+ quietTicks = 0;
241
+ } catch (err) {
242
+ console.error(`\natris live paused after ${label}: ${err.message || err}`);
243
+ console.error('Fix the issue, then restart `atris live`.');
244
+ process.exit(1);
245
+ } finally {
246
+ running = false;
247
+ }
248
+ }
249
+
250
+ setInterval(() => {
251
+ if (running) return;
252
+ const current = collectSnapshot(options.cwd);
253
+ if (snapshotsDiffer(lastSnapshot, current)) {
254
+ pendingPush = true;
255
+ quietTicks = 0;
256
+ lastSnapshot = current;
257
+ process.stdout.write('\rLocal brain changed. Waiting for quiet before push... ');
258
+ return;
259
+ }
260
+ if (pendingPush) {
261
+ quietTicks += 1;
262
+ if (quietTicks >= options.debounceSec) {
263
+ void guarded('push', async () => {
264
+ console.log('\nLocal brain quiet. Pushing fresh state...');
265
+ if (!options.noPush) await runCli(buildPushArgs(options), options);
266
+ });
267
+ }
268
+ }
269
+ }, 1000);
270
+
271
+ setInterval(() => {
272
+ if (pendingPush) {
273
+ process.stdout.write('\rLocal changes pending; skipping cloud pull until local brain is pushed... ');
274
+ return;
275
+ }
276
+ void guarded('periodic pull', async () => {
277
+ console.log('\nChecking cloud for fresher brain...');
278
+ await runCli(buildPullArgs(options), options);
279
+ });
280
+ }, options.intervalSec * 1000);
281
+ }
282
+
283
+ module.exports = {
284
+ collectSnapshot,
285
+ liveCommand,
286
+ parseLiveOptions,
287
+ shouldIgnore,
288
+ snapshotsDiffer,
289
+ };
package/commands/pull.js CHANGED
@@ -6,7 +6,7 @@ const { findAllMembers } = require('./member');
6
6
  const { loadConfig } = require('../utils/config');
7
7
  const { getLogPath } = require('../lib/file-ops');
8
8
  const { parseJournalSections, mergeSections, reconstructJournal } = require('../lib/journal');
9
- const { loadBusinesses } = require('./business');
9
+ const { loadBusinesses, businessMatchesSlug } = require('./business');
10
10
  const { loadManifest, saveManifest, computeFileHash, buildManifest, computeLocalHashes, threeWayCompare } = require('../lib/manifest');
11
11
  const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
12
12
  const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
@@ -204,6 +204,7 @@ async function pullBusiness(slug) {
204
204
 
205
205
  // Resolve business ID — always refresh from API to avoid stale workspace_id
206
206
  let businessId, workspaceId, businessName, resolvedSlug;
207
+ let localSlug = slug;
207
208
  const businesses = loadBusinesses();
208
209
 
209
210
  const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
@@ -220,7 +221,7 @@ async function pullBusiness(slug) {
220
221
  }
221
222
  } else {
222
223
  const match = (listResult.data || []).find(
223
- b => b.slug === slug || b.name.toLowerCase() === slug.toLowerCase()
224
+ b => businessMatchesSlug(b, slug, { includeName: true })
224
225
  );
225
226
  if (!match) {
226
227
  console.error(`Business "${slug}" not found.`);
@@ -230,13 +231,15 @@ async function pullBusiness(slug) {
230
231
  workspaceId = match.workspace_id;
231
232
  businessName = match.name;
232
233
  resolvedSlug = match.slug;
234
+ localSlug = businessMatchesSlug(match, slug) ? slug : match.slug;
233
235
 
234
236
  // Update local cache
235
237
  businesses[slug] = {
236
238
  business_id: businessId,
237
239
  workspace_id: workspaceId,
238
240
  name: businessName,
239
- slug: match.slug,
241
+ slug: localSlug,
242
+ canonical_slug: match.slug,
240
243
  added_at: new Date().toISOString(),
241
244
  };
242
245
  const { saveBusinesses } = require('./business');
@@ -697,7 +700,8 @@ async function pullBusiness(slug) {
697
700
  const atrisDir = path.join(outputDir, '.atris');
698
701
  fs.mkdirSync(atrisDir, { recursive: true });
699
702
  fs.writeFileSync(path.join(atrisDir, 'business.json'), JSON.stringify({
700
- slug: resolvedSlug || slug,
703
+ slug: localSlug,
704
+ canonical_slug: resolvedSlug || slug,
701
705
  business_id: businessId,
702
706
  workspace_id: workspaceId,
703
707
  name: businessName,
package/commands/push.js CHANGED
@@ -3,7 +3,7 @@ const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const { loadCredentials } = require('../utils/auth');
5
5
  const { apiRequestJson } = require('../utils/api');
6
- const { loadBusinesses, saveBusinesses } = require('./business');
6
+ const { loadBusinesses, saveBusinesses, businessMatchesSlug } = require('./business');
7
7
  const { loadManifest, saveManifest, buildManifest, computeLocalHashes } = require('../lib/manifest');
8
8
  const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
9
9
  const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
@@ -92,7 +92,7 @@ async function pushAtris() {
92
92
  const businesses = loadBusinesses();
93
93
  const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
94
94
  if (listResult.ok) {
95
- const match = (listResult.data || []).find(b => b.slug === slug || b.name.toLowerCase() === slug.toLowerCase());
95
+ const match = (listResult.data || []).find(b => businessMatchesSlug(b, slug, { includeName: true }));
96
96
  if (!match) { console.error(`Business "${slug}" not found.`); process.exit(1); }
97
97
  businessId = match.id;
98
98
  workspaceId = match.workspace_id;