atris 3.82.14 → 3.82.16
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/bin/atris.js +10 -6
- package/commands/auth.js +149 -35
- package/commands/business.js +20 -19
- package/commands/memory-live.js +16 -1
- package/commands/push.js +17 -3
- package/lib/memory-live.js +27 -3
- package/package.json +1 -1
package/bin/atris.js
CHANGED
|
@@ -153,7 +153,7 @@ const dryRunRequested = updateArgs.includes('--dry-run');
|
|
|
153
153
|
const skipUpdateCheck = Boolean(process.env.ATRIS_SKIP_UPDATE_CHECK || process.env.NO_UPDATE_NOTIFIER || helpRequested || jsonRequested);
|
|
154
154
|
// Local-only commands make no registry call and start no background install.
|
|
155
155
|
// sentinel prints its own one-line notice at the end of the run instead.
|
|
156
|
-
if (!skipUpdateCheck && (!updateCommand || (updateCommand && !['version', 'update', 'mcp', 'recap', '99', 'sentinel'].includes(updateCommand)))) {
|
|
156
|
+
if (!skipUpdateCheck && (!updateCommand || (updateCommand && !['version', 'update', 'mcp', 'recap', '99', 'sentinel', 'login'].includes(updateCommand)))) {
|
|
157
157
|
updateCheckPromise = checkForUpdates()
|
|
158
158
|
.then((updateInfo) => {
|
|
159
159
|
if (updateInfo) {
|
|
@@ -1047,7 +1047,7 @@ function showReleaseHelp() {
|
|
|
1047
1047
|
|
|
1048
1048
|
function showAuthHelp(commandName) {
|
|
1049
1049
|
const usage = {
|
|
1050
|
-
login: 'Usage: atris login [--token <token>] [--force] [--agent] [--client-id <atc_...> --client-secret-file <path>]',
|
|
1050
|
+
login: 'Usage: atris login [--code <code>] [--token <token>] [--force] [--agent] [--client-id <atc_...> --client-secret-file <path>]',
|
|
1051
1051
|
logout: 'Usage: atris logout',
|
|
1052
1052
|
whoami: 'Usage: atris whoami [--json]',
|
|
1053
1053
|
switch: 'Usage: atris switch [account] [--global]',
|
|
@@ -1059,10 +1059,13 @@ function showAuthHelp(commandName) {
|
|
|
1059
1059
|
console.log('');
|
|
1060
1060
|
console.log('Description:');
|
|
1061
1061
|
if (commandName === 'login') {
|
|
1062
|
-
console.log('
|
|
1063
|
-
console.log('
|
|
1064
|
-
console.log('
|
|
1065
|
-
console.log('
|
|
1062
|
+
console.log(' Pick the one that fits you:');
|
|
1063
|
+
console.log(' A person: run atris login. It opens the browser, then you paste the one-time code.');
|
|
1064
|
+
console.log(' No terminal? Open atris.ai/auth/cli and run atris login --code <code>.');
|
|
1065
|
+
console.log(' An agent on its own: atris login --client-id <atc_...> --client-secret-file <path>.');
|
|
1066
|
+
console.log(' Make the client once with atris agent-client. It renews itself and never prints the secret.');
|
|
1067
|
+
console.log(' A script calling the API: atris login --token <atris_... key>.');
|
|
1068
|
+
console.log(' The one-time code is not a token. If you paste it as one, login signs you in with it.');
|
|
1066
1069
|
} else if (commandName === 'logout') {
|
|
1067
1070
|
console.log(' Sign out of the current Atris account.');
|
|
1068
1071
|
} else if (commandName === 'whoami') {
|
|
@@ -1077,6 +1080,7 @@ function showAuthHelp(commandName) {
|
|
|
1077
1080
|
console.log('');
|
|
1078
1081
|
console.log('Options:');
|
|
1079
1082
|
if (commandName === 'login') {
|
|
1083
|
+
console.log(' --code <code> Sign in with the one-time code from atris.ai/auth/cli. --code - reads it from stdin.');
|
|
1080
1084
|
console.log(' --token <token> Save an API token without prompting.');
|
|
1081
1085
|
console.log(' --force, -f Re-run login even if credentials already exist.');
|
|
1082
1086
|
console.log(' --agent Mint a scoped agent token from the stored login.');
|
package/commands/auth.js
CHANGED
|
@@ -622,6 +622,108 @@ async function loginAgentClient(request, deps = {}) {
|
|
|
622
622
|
return 0;
|
|
623
623
|
}
|
|
624
624
|
|
|
625
|
+
// The browser page at /auth/cli shows a one-time code: secrets.token_urlsafe(32)
|
|
626
|
+
// in the backend, so 43 url-safe characters with no dots. Tokens are JWTs
|
|
627
|
+
// (three dot-joined parts) and API keys start with atris_. A code pasted where
|
|
628
|
+
// a token belongs used to be saved as a token and fail later with a JWT error.
|
|
629
|
+
const CLI_CODE_PATTERN = /^(?!atris_)[A-Za-z0-9_-]{43}$/;
|
|
630
|
+
const CODE_NOT_TOKEN_NOTE = 'that is the one-time code from the browser, not a token. signing you in with it.';
|
|
631
|
+
|
|
632
|
+
function looksLikeCliCode(value) {
|
|
633
|
+
return CLI_CODE_PATTERN.test(String(value || '').trim());
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// readFlag treats a value starting with "-" as the next flag, but one code in
|
|
637
|
+
// 64 starts with a dash. Take --code=<v> as is, and the next arg when it is
|
|
638
|
+
// code-shaped or not a flag. "-" or nothing means read stdin.
|
|
639
|
+
function readCodeFlag(args = []) {
|
|
640
|
+
const eq = args.find((arg) => String(arg).startsWith('--code='));
|
|
641
|
+
if (eq !== undefined) {
|
|
642
|
+
const value = String(eq).slice('--code='.length);
|
|
643
|
+
return value === '-' ? '' : value;
|
|
644
|
+
}
|
|
645
|
+
const next = args[args.indexOf('--code') + 1];
|
|
646
|
+
if (next === undefined || next === '-') return '';
|
|
647
|
+
if (looksLikeCliCode(next) || !String(next).startsWith('-')) return String(next);
|
|
648
|
+
return '';
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function headlessLoginHelp() {
|
|
652
|
+
const url = `${getAppBaseUrl()}/auth/cli`;
|
|
653
|
+
return [
|
|
654
|
+
`open ${url}, sign in, then pass the one-time code it shows:`,
|
|
655
|
+
' atris login --code <code> or echo <code> | atris login',
|
|
656
|
+
'agents without a person: atris login --client-id <atc_...> --client-secret-file <path>',
|
|
657
|
+
];
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Reads a code piped into stdin. Only reads a pipe, socket, or file, and
|
|
661
|
+
// gives up after a short wait so an open, silent stdin never hangs login.
|
|
662
|
+
function readPipedLoginInput(timeoutMs = 1500) {
|
|
663
|
+
return new Promise((resolve) => {
|
|
664
|
+
let stat;
|
|
665
|
+
try { stat = fs.fstatSync(0); } catch { resolve(''); return; }
|
|
666
|
+
// Node-launched agents hand stdin over as a socket, shells as a FIFO.
|
|
667
|
+
if (!stat || !(stat.isFIFO() || stat.isFile() || stat.isSocket())) { resolve(''); return; }
|
|
668
|
+
let data = '';
|
|
669
|
+
let finished = false;
|
|
670
|
+
const finish = () => {
|
|
671
|
+
if (finished) return;
|
|
672
|
+
finished = true;
|
|
673
|
+
clearTimeout(timer);
|
|
674
|
+
process.stdin.removeAllListeners('data');
|
|
675
|
+
process.stdin.pause();
|
|
676
|
+
resolve(data.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '');
|
|
677
|
+
};
|
|
678
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
679
|
+
if (timer.unref) timer.unref();
|
|
680
|
+
process.stdin.setEncoding('utf8');
|
|
681
|
+
process.stdin.on('data', (chunk) => {
|
|
682
|
+
data += chunk;
|
|
683
|
+
if (data.length > 8192 || /\n/.test(data)) finish();
|
|
684
|
+
});
|
|
685
|
+
process.stdin.on('end', finish);
|
|
686
|
+
process.stdin.on('error', finish);
|
|
687
|
+
process.stdin.resume();
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async function exchangeCliCodeAndSave(code, { asJson = false, note = null, quiet = false } = {}) {
|
|
692
|
+
if (note && !asJson) console.log(note);
|
|
693
|
+
const exchange = await apiRequestJson('/auth/cli/exchange', {
|
|
694
|
+
method: 'POST',
|
|
695
|
+
body: { code: String(code).trim() },
|
|
696
|
+
});
|
|
697
|
+
const payload = exchange && exchange.ok ? exchange.data : null;
|
|
698
|
+
if (!payload || !payload.token || !payload.refresh_token) {
|
|
699
|
+
const error = !payload
|
|
700
|
+
? (exchange && exchange.error) || 'invalid or expired code'
|
|
701
|
+
: 'the server did not return tokens. try again.';
|
|
702
|
+
const unreachable = !exchange || !exchange.status;
|
|
703
|
+
const next = unreachable
|
|
704
|
+
? `could not reach ${getApiBaseUrl()}. check the connection and run the same command again.`
|
|
705
|
+
: `get a fresh code at ${getAppBaseUrl()}/auth/cli. codes last five minutes and work once.`;
|
|
706
|
+
if (asJson) {
|
|
707
|
+
console.log(JSON.stringify({ ok: false, logged_in: false, error, next }, null, 2));
|
|
708
|
+
} else {
|
|
709
|
+
console.error(`Error: ${error}`);
|
|
710
|
+
console.error(next);
|
|
711
|
+
}
|
|
712
|
+
return 1;
|
|
713
|
+
}
|
|
714
|
+
const email = payload.email || null;
|
|
715
|
+
saveCredentials(payload.token, payload.refresh_token, email, payload.user_id || null, payload.provider || 'atris');
|
|
716
|
+
if (quiet) return 0;
|
|
717
|
+
if (asJson) {
|
|
718
|
+
console.log(JSON.stringify({ ok: true, logged_in: true, email, user_id: payload.user_id || null, provider: payload.provider || 'atris', next: 'atris whoami --json' }, null, 2));
|
|
719
|
+
return 0;
|
|
720
|
+
}
|
|
721
|
+
const name = profileNameFromEmail(email);
|
|
722
|
+
console.log(`\n✓ Signed in as ${email || 'unknown'}${name ? ` (profile: ${name})` : ''}`);
|
|
723
|
+
await displayAccountSummary(apiRequestJson);
|
|
724
|
+
return 0;
|
|
725
|
+
}
|
|
726
|
+
|
|
625
727
|
async function loginAtris(options = {}) {
|
|
626
728
|
// Support: atris login --token <token> --force
|
|
627
729
|
const args = process.argv.slice(3);
|
|
@@ -645,7 +747,11 @@ async function loginAtris(options = {}) {
|
|
|
645
747
|
const existing = loadCredentials();
|
|
646
748
|
|
|
647
749
|
if (wantsAgentToken(args)) {
|
|
648
|
-
if (directToken) {
|
|
750
|
+
if (directToken && looksLikeCliCode(directToken)) {
|
|
751
|
+
// sign in with the code first, then mint from that real login
|
|
752
|
+
const exchanged = await exchangeCliCodeAndSave(directToken, { asJson, note: CODE_NOT_TOKEN_NOTE, quiet: true });
|
|
753
|
+
if (exchanged !== 0) process.exit(exchanged);
|
|
754
|
+
} else if (directToken) {
|
|
649
755
|
saveCredentials(
|
|
650
756
|
String(directToken).trim(),
|
|
651
757
|
existing?.refresh_token || null,
|
|
@@ -658,7 +764,26 @@ async function loginAtris(options = {}) {
|
|
|
658
764
|
process.exit(code);
|
|
659
765
|
}
|
|
660
766
|
|
|
767
|
+
// One-time code from the browser, no prompt: --code <code>, --code=<code>,
|
|
768
|
+
// or --code - to read it from stdin. An explicit code means sign in now,
|
|
769
|
+
// even over an existing login.
|
|
770
|
+
const codeFlagPresent = args.some((arg) => arg === '--code' || String(arg).startsWith('--code='));
|
|
771
|
+
if (codeFlagPresent) {
|
|
772
|
+
let code = readCodeFlag(args);
|
|
773
|
+
if (!code) code = await readPipedLoginInput(5000);
|
|
774
|
+
if (!code) {
|
|
775
|
+
const error = 'no code given.';
|
|
776
|
+
if (asJson) console.log(JSON.stringify({ ok: false, logged_in: false, error, next: 'atris login --code <code>' }, null, 2));
|
|
777
|
+
else { console.error(`Error: ${error}`); headlessLoginHelp().forEach((line) => console.error(line)); }
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
process.exit(await exchangeCliCodeAndSave(code, { asJson }));
|
|
781
|
+
}
|
|
782
|
+
|
|
661
783
|
// Direct token mode (non-interactive)
|
|
784
|
+
if (directToken && looksLikeCliCode(directToken)) {
|
|
785
|
+
process.exit(await exchangeCliCodeAndSave(directToken, { asJson, note: CODE_NOT_TOKEN_NOTE }));
|
|
786
|
+
}
|
|
662
787
|
if (directToken) {
|
|
663
788
|
const trimmed = directToken.trim();
|
|
664
789
|
saveCredentials(trimmed, null, existing?.email || null, existing?.user_id || null, existing?.provider || 'manual');
|
|
@@ -675,6 +800,13 @@ async function loginAtris(options = {}) {
|
|
|
675
800
|
process.exit(0);
|
|
676
801
|
}
|
|
677
802
|
|
|
803
|
+
// echo <code> | atris login signs in with no terminal, over an existing
|
|
804
|
+
// login too. Read once here so the shortcut below cannot swallow it.
|
|
805
|
+
const piped = (!process.stdin.isTTY && !directToken) ? await readPipedLoginInput() : '';
|
|
806
|
+
if (piped && looksLikeCliCode(piped)) {
|
|
807
|
+
process.exit(await exchangeCliCodeAndSave(piped, { asJson }));
|
|
808
|
+
}
|
|
809
|
+
|
|
678
810
|
// Already signed in without --force: behave like whoami (no menu).
|
|
679
811
|
// Print local identity without waiting on the network so headless agents
|
|
680
812
|
// never hang on a stale token validation.
|
|
@@ -706,13 +838,17 @@ async function loginAtris(options = {}) {
|
|
|
706
838
|
console.log(JSON.stringify({
|
|
707
839
|
ok: false,
|
|
708
840
|
logged_in: false,
|
|
709
|
-
error:
|
|
710
|
-
|
|
841
|
+
error: piped
|
|
842
|
+
? 'what came in on stdin is not a one-time code from the browser'
|
|
843
|
+
: 'login has no terminal here. pass the one-time code from the browser.',
|
|
844
|
+
next: 'atris login --code <code>',
|
|
845
|
+
code_url: `${getAppBaseUrl()}/auth/cli`,
|
|
846
|
+
agents: 'atris login --client-id <atc_...> --client-secret-file <path>',
|
|
711
847
|
}, null, 2));
|
|
712
848
|
} else {
|
|
713
849
|
console.log('Not signed in.');
|
|
714
|
-
console.log('
|
|
715
|
-
console.log(
|
|
850
|
+
if (piped) console.log('what came in on stdin is not a one-time code from the browser. for an API token use --token.');
|
|
851
|
+
headlessLoginHelp().forEach((line) => console.log(line));
|
|
716
852
|
}
|
|
717
853
|
process.exit(1);
|
|
718
854
|
}
|
|
@@ -723,7 +859,7 @@ async function loginAtris(options = {}) {
|
|
|
723
859
|
|
|
724
860
|
console.log('Choose login method:');
|
|
725
861
|
console.log(' 1. Browser OAuth (recommended)');
|
|
726
|
-
console.log(' 2. Paste
|
|
862
|
+
console.log(' 2. Paste an API token');
|
|
727
863
|
console.log(' 3. Cancel');
|
|
728
864
|
|
|
729
865
|
const methodChoice = await promptUser('\nEnter choice (1-3): ');
|
|
@@ -743,39 +879,17 @@ async function loginAtris(options = {}) {
|
|
|
743
879
|
process.exit(1);
|
|
744
880
|
}
|
|
745
881
|
|
|
746
|
-
|
|
747
|
-
method: 'POST',
|
|
748
|
-
body: { code: code.trim() },
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
if (!exchange.ok || !exchange.data) {
|
|
752
|
-
console.error(`Error: ${exchange.error || 'Invalid or expired code'}`);
|
|
753
|
-
process.exit(1);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const payload = exchange.data;
|
|
757
|
-
const token = payload.token;
|
|
758
|
-
const refreshToken = payload.refresh_token;
|
|
759
|
-
|
|
760
|
-
if (!token || !refreshToken) {
|
|
761
|
-
console.error('Error: Backend did not return tokens. Try again.');
|
|
762
|
-
process.exit(1);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const email = payload.email || null;
|
|
766
|
-
const userId = payload.user_id || null;
|
|
767
|
-
const provider = payload.provider || 'atris';
|
|
768
|
-
|
|
769
|
-
saveCredentials(token, refreshToken, email, userId, provider);
|
|
770
|
-
const name = profileNameFromEmail(email);
|
|
771
|
-
console.log(`\n✓ Signed in as ${email || 'unknown'}${name ? ` (profile: ${name})` : ''}`);
|
|
772
|
-
await displayAccountSummary(apiRequestJson);
|
|
773
|
-
process.exit(0);
|
|
882
|
+
process.exit(await exchangeCliCodeAndSave(code));
|
|
774
883
|
} else if (methodChoice === '2') {
|
|
775
|
-
console.log('\
|
|
884
|
+
console.log('\nPaste an API token (starts with atris_) or a saved session token.');
|
|
885
|
+
console.log('The one-time code from the browser works here too.\n');
|
|
776
886
|
|
|
777
887
|
const tokenInput = await promptUser('API token: ', { secret: true });
|
|
778
888
|
|
|
889
|
+
if (tokenInput && looksLikeCliCode(tokenInput)) {
|
|
890
|
+
process.exit(await exchangeCliCodeAndSave(tokenInput, { note: CODE_NOT_TOKEN_NOTE }));
|
|
891
|
+
}
|
|
892
|
+
|
|
779
893
|
if (!tokenInput) {
|
|
780
894
|
console.error('Error: Token is required.');
|
|
781
895
|
process.exit(1);
|
package/commands/business.js
CHANGED
|
@@ -52,38 +52,38 @@ const {
|
|
|
52
52
|
findAtrisDir,
|
|
53
53
|
} = require('../lib/business-core');
|
|
54
54
|
|
|
55
|
-
async function findExistingBusinessBySlug(slug, token) {
|
|
55
|
+
async function findExistingBusinessBySlug(slug, token, { api = apiRequestJson, loadLocal = loadBusinesses } = {}) {
|
|
56
56
|
if (!slug) return null;
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return { id: v.business_id, name: v.name, slug, source: 'local' };
|
|
66
|
-
}
|
|
67
|
-
}
|
|
58
|
+
const local = loadLocal();
|
|
59
|
+
const localMatch = local[slug]
|
|
60
|
+
? { id: local[slug].business_id, name: local[slug].name, slug, source: 'local' }
|
|
61
|
+
: (() => {
|
|
62
|
+
const v = Object.values(local).find((b) => businessMatchesSlug(b, slug));
|
|
63
|
+
return v ? { id: v.business_id, name: v.name, slug, source: 'local' } : null;
|
|
64
|
+
})();
|
|
68
65
|
|
|
69
|
-
if (!token) return
|
|
66
|
+
if (!token) return localMatch;
|
|
70
67
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
token,
|
|
75
|
-
});
|
|
68
|
+
// The cloud is the truth. The Mac's list can hold a business that was
|
|
69
|
+
// deleted, and trusting it refused to recreate one that no longer existed.
|
|
70
|
+
const direct = await api(`/business/by-slug/${encodeURIComponent(slug)}`, { method: 'GET', token });
|
|
76
71
|
if (direct.ok && direct.data && direct.data.id) {
|
|
77
72
|
return { id: direct.data.id, name: direct.data.name, slug: direct.data.slug || slug, source: 'cloud' };
|
|
78
73
|
}
|
|
79
74
|
|
|
80
|
-
const list = await
|
|
75
|
+
const list = await api('/business/', { method: 'GET', token });
|
|
81
76
|
if (list.ok && Array.isArray(list.data)) {
|
|
82
77
|
const match = list.data.find(b => businessMatchesSlug(b, slug));
|
|
83
78
|
if (match) return { id: match.id, name: match.name, slug: match.slug, source: 'cloud' };
|
|
79
|
+
if (localMatch) {
|
|
80
|
+
console.warn(` Note: this Mac still lists "${slug}", but the cloud has no such business. Run: atris business doctor --fix`);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
|
|
85
|
+
// Cloud unreachable: fall back to the Mac's list rather than risk a duplicate.
|
|
86
|
+
return localMatch;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
async function addBusiness(slug) {
|
|
@@ -1665,6 +1665,7 @@ async function businessCommand(subcommand, ...args) {
|
|
|
1665
1665
|
}
|
|
1666
1666
|
|
|
1667
1667
|
module.exports = {
|
|
1668
|
+
findExistingBusinessBySlug,
|
|
1668
1669
|
businessCommand,
|
|
1669
1670
|
businessWorkspaceBase,
|
|
1670
1671
|
loadBusinesses,
|
package/commands/memory-live.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// atris memory show, search, read, ls, remember, write, rm, usage, log, lesson
|
|
3
|
+
// atris memory show, search, ask, read, ls, remember, write, rm, usage, log, lesson
|
|
4
4
|
// and agents: the person's Atris memory from any terminal, so any agent can
|
|
5
5
|
// read it, search it and save to it. One request per command. The login and
|
|
6
6
|
// network modules load only when a command needs the server.
|
|
@@ -13,6 +13,7 @@ const { NOT_LOGGED_IN, oneLine, tokenFrom } = require('../lib/developer-api');
|
|
|
13
13
|
const MAX_WRITE_BYTES = 1000000;
|
|
14
14
|
const STDIN_WAIT_MS = 5000;
|
|
15
15
|
const REMEMBER_MAX_CHARS = 1000;
|
|
16
|
+
const ASK_MAX_CHARS = 500;
|
|
16
17
|
const REMEMBER_FILES = ['journal', 'now', 'learning'];
|
|
17
18
|
const LEARNING_SECTIONS = ['want', 'understand', 'shaky'];
|
|
18
19
|
|
|
@@ -138,6 +139,20 @@ const COMMANDS = {
|
|
|
138
139
|
},
|
|
139
140
|
},
|
|
140
141
|
|
|
142
|
+
ask: {
|
|
143
|
+
check(ctx) {
|
|
144
|
+
const question = ctx.words.join(' ').replace(/\s+/g, ' ').trim();
|
|
145
|
+
if (!question) throw new UsageError('ask a question, like atris memory ask "where does Sam work?"');
|
|
146
|
+
if (question.length > ASK_MAX_CHARS) throw new UsageError('keep the question under 500 characters');
|
|
147
|
+
return { question, folder: ctx.flags['--in'] || '' };
|
|
148
|
+
},
|
|
149
|
+
async run(ctx, { question, folder }) {
|
|
150
|
+
// The server reads for up to 75 seconds; asking twice would count twice.
|
|
151
|
+
const data = await ctx.call(`/memory/ask?q=${enc(question)}${folder ? `&folder=${enc(folder)}` : ''}`, { timeoutMs: 90000, retries: 0 });
|
|
152
|
+
return { data, lines: live.askLines(data) };
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
|
|
141
156
|
read: {
|
|
142
157
|
check(ctx) {
|
|
143
158
|
const rel = onePath(ctx, 'file to read');
|
package/commands/push.js
CHANGED
|
@@ -9,6 +9,17 @@ const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
|
|
|
9
9
|
const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
|
|
10
10
|
const { assertSafeWorkspaceRoot } = require('../lib/workspace-safety');
|
|
11
11
|
|
|
12
|
+
// The server's own words for a refused request. It used to be dropped, so a
|
|
13
|
+
// refusal for any reason read as "you can only push to your team/ folder".
|
|
14
|
+
function refusalText(result) {
|
|
15
|
+
if (!result) return '';
|
|
16
|
+
const raw = result.errorMessage || result.error || (result.data && result.data.detail) || '';
|
|
17
|
+
if (!raw) return '';
|
|
18
|
+
if (typeof raw === 'string') return raw === 'Request failed' ? '' : raw;
|
|
19
|
+
if (typeof raw === 'object' && typeof raw.message === 'string') return raw.message;
|
|
20
|
+
try { return JSON.stringify(raw); } catch { return String(raw); }
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
function resolvePushSourceDir({ slug, argv = process.argv, cwd = process.cwd() }) {
|
|
13
24
|
const fromIdx = argv.indexOf('--from');
|
|
14
25
|
if (fromIdx !== -1 && argv[fromIdx + 1]) {
|
|
@@ -858,13 +869,15 @@ async function pushAtris() {
|
|
|
858
869
|
pushed = landedPaths.size;
|
|
859
870
|
break;
|
|
860
871
|
} else {
|
|
861
|
-
console.error(`\n Push failed: ${retry
|
|
872
|
+
console.error(`\n Push failed: ${refusalText(retry) || retry.status}`);
|
|
873
|
+
if (refusalText(result)) console.error(` The first refusal said: ${refusalText(result)}`);
|
|
862
874
|
await emit('access_denied', { error_detail: `403 retry failed: ${retry.status}` });
|
|
863
875
|
process.exit(1);
|
|
864
876
|
}
|
|
865
877
|
} else {
|
|
866
|
-
|
|
867
|
-
|
|
878
|
+
const reason = refusalText(result);
|
|
879
|
+
console.error(`\n Access denied: ${reason || 'the server refused this push and gave no reason.'}`);
|
|
880
|
+
await emit('access_denied', reason ? { error_detail: reason } : undefined);
|
|
868
881
|
process.exit(1);
|
|
869
882
|
}
|
|
870
883
|
} else if (result.status === 409) {
|
|
@@ -1052,6 +1065,7 @@ async function pushAtris() {
|
|
|
1052
1065
|
}
|
|
1053
1066
|
|
|
1054
1067
|
module.exports = {
|
|
1068
|
+
refusalText,
|
|
1055
1069
|
pushAtris,
|
|
1056
1070
|
buildPushChangePlan,
|
|
1057
1071
|
buildPushUploadBatches,
|
package/lib/memory-live.js
CHANGED
|
@@ -9,7 +9,7 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
|
|
11
11
|
const LIVE_COMMANDS = new Set([
|
|
12
|
-
'show', 'search', 'read', 'ls', 'remember', 'write', 'rm', 'usage', 'log', 'lesson', 'agents', 'setup',
|
|
12
|
+
'show', 'search', 'ask', 'read', 'ls', 'remember', 'write', 'rm', 'usage', 'log', 'lesson', 'agents', 'setup',
|
|
13
13
|
]);
|
|
14
14
|
|
|
15
15
|
function isLiveCommand(word) {
|
|
@@ -19,10 +19,11 @@ function isLiveCommand(word) {
|
|
|
19
19
|
// One line per command, shown together by `atris memory` and one at a time by
|
|
20
20
|
// `atris memory <command> --help`.
|
|
21
21
|
const COLUMN = 38;
|
|
22
|
-
const row = (form, ...what) => [` ${form}`.padEnd(COLUMN) + what[0], ...what.slice(1).map((more) => ' '.repeat(COLUMN) + more)].join('\n');
|
|
22
|
+
const row = (form, ...what) => [` ${form} `.padEnd(COLUMN) + what[0], ...what.slice(1).map((more) => ' '.repeat(COLUMN) + more)].join('\n');
|
|
23
23
|
const USAGE_LINES = {
|
|
24
24
|
show: row('show', 'who they are and what matters now; read it before helping'),
|
|
25
25
|
search: row('search "rent|lease" [--in folder]', 'lines with any of the words, split by |'),
|
|
26
|
+
ask: row('ask "a question" [--in folder]', 'answered from their memory, with the lines it rests on'),
|
|
26
27
|
read: row('read <path> [--from N] [--lines N]', 'numbered lines of one file; --raw prints it as stored'),
|
|
27
28
|
ls: row('ls [folder]', 'what is in a folder; MAP.md says what is where'),
|
|
28
29
|
remember: row('remember "one short line"', 'a lasting fact or preference about the person, when they',
|
|
@@ -43,6 +44,7 @@ const LIVE_HELP = [
|
|
|
43
44
|
'read',
|
|
44
45
|
USAGE_LINES.show,
|
|
45
46
|
USAGE_LINES.search,
|
|
47
|
+
USAGE_LINES.ask,
|
|
46
48
|
USAGE_LINES.read,
|
|
47
49
|
USAGE_LINES.ls,
|
|
48
50
|
'',
|
|
@@ -62,6 +64,7 @@ const LIVE_HELP = [
|
|
|
62
64
|
// Flags that take a value, per command. --json, --raw and --help take none.
|
|
63
65
|
const VALUE_FLAGS = {
|
|
64
66
|
search: ['--in'],
|
|
67
|
+
ask: ['--in'],
|
|
65
68
|
read: ['--from', '--lines'],
|
|
66
69
|
ls: [],
|
|
67
70
|
show: [],
|
|
@@ -205,6 +208,26 @@ function searchLines(query, data) {
|
|
|
205
208
|
return lines;
|
|
206
209
|
}
|
|
207
210
|
|
|
211
|
+
// The answer first, then each line it rests on, then how sure it is. With no
|
|
212
|
+
// answer (the model was busy) the search hits it started from are shown.
|
|
213
|
+
function askLines(data) {
|
|
214
|
+
const sources = (data && data.sources) || [];
|
|
215
|
+
const cite = sources.map((s) => ` ${s.path}:${s.n}: ${s.text}`);
|
|
216
|
+
if (data && data.answer && data.found) {
|
|
217
|
+
return [String(data.answer), ...(cite.length ? ['', 'From:', ...cite] : [])];
|
|
218
|
+
}
|
|
219
|
+
if (data && data.answer) {
|
|
220
|
+
const lines = [String(data.answer)];
|
|
221
|
+
if (cite.length) lines.push('', 'Closest lines:', ...cite);
|
|
222
|
+
lines.push('', 'Not found for certain. Search it yourself: atris memory search "word|other word"');
|
|
223
|
+
return lines;
|
|
224
|
+
}
|
|
225
|
+
const hits = (data && data.search && data.search.hits) || [];
|
|
226
|
+
const lines = [String((data && data.note) || 'No answer right now.')];
|
|
227
|
+
if (hits.length) lines.push('', 'Lines that may hold it:', ...hits.slice(0, 8).map((h) => ` ${h.path}:${h.n}: ${h.text}`));
|
|
228
|
+
return lines;
|
|
229
|
+
}
|
|
230
|
+
|
|
208
231
|
function readLines(data) {
|
|
209
232
|
const page = (data && data.lines) || [];
|
|
210
233
|
const total = Number(data && data.total) || 0;
|
|
@@ -244,7 +267,7 @@ function rulesBlock(agent) {
|
|
|
244
267
|
'',
|
|
245
268
|
'This person keeps one memory in Atris that every app and agent shares. Use it.',
|
|
246
269
|
'- Start each task with `atris memory show`: who they are and what matters now.',
|
|
247
|
-
'- Before you ask them anything they may have said before, search: `atris memory search "word|other word"`.',
|
|
270
|
+
'- Before you ask them anything they may have said before, search: `atris memory search "word|other word"`, or ask in plain words: `atris memory ask "..."`.',
|
|
248
271
|
'- Open what you find with `atris memory read <path>`. `atris memory ls` shows the folder.',
|
|
249
272
|
'- When they say remember, note, save or don\'t forget, save one short line: `atris memory remember "..."`.',
|
|
250
273
|
'- Save only lasting facts about them, never logs or your own work. Never rewrite their about.md.',
|
|
@@ -338,6 +361,7 @@ module.exports = {
|
|
|
338
361
|
END,
|
|
339
362
|
LIVE_HELP,
|
|
340
363
|
USAGE_LINES,
|
|
364
|
+
askLines,
|
|
341
365
|
isLiveCommand,
|
|
342
366
|
lsLines,
|
|
343
367
|
mcpLines,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.82.
|
|
3
|
+
"version": "3.82.16",
|
|
4
4
|
"description": "you say what you want in plain words. atris builds it, checks it, and shows you proof. project memory your coding agents share across sessions.",
|
|
5
5
|
"mcpName": "io.github.atrislabs/atris",
|
|
6
6
|
"main": "bin/atris.js",
|