atris 3.82.25 → 3.83.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/AGENTS.md +10 -12
- package/README.md +77 -0
- package/bin/atris.js +95 -44
- package/commands/activate.js +39 -33
- package/commands/agent-home.js +403 -0
- package/commands/agent-signin.js +214 -0
- package/commands/agent-spawn.js +1 -0
- package/commands/autoland.js +173 -71
- package/commands/brain.js +211 -410
- package/commands/doctor.js +38 -26
- package/commands/init.js +4 -4
- package/commands/join.js +159 -0
- package/commands/memory.js +2 -2
- package/commands/morning.js +132 -0
- package/commands/night.js +178 -0
- package/commands/recap.js +4 -4
- package/commands/task.js +31 -2
- package/commands/worktree.js +18 -0
- package/lib/agent-home.js +289 -0
- package/lib/agent-page.js +1257 -0
- package/lib/cleanup-summary.js +4 -2
- package/lib/first-minute.js +41 -20
- package/lib/gm-phone-page.js +8 -2
- package/lib/known-commands.js +1 -1
- package/lib/member-autoland.js +39 -13
- package/lib/member-export.js +31 -0
- package/lib/memory-folder.js +122 -24
- package/lib/memory-live.js +2 -2
- package/lib/night-learned.js +171 -0
- package/lib/night-shift.js +123 -0
- package/lib/task-core.js +2 -1
- package/lib/task-db.js +32 -0
- package/package.json +1 -1
- package/templates/members/designer-1/MEMBER.md +34 -0
- package/templates/members/designer-1/MISSION.md +11 -0
- package/templates/members/designer-1/SOUL.md +11 -0
- package/templates/members/designer-1/context/why.md +5 -0
- package/templates/members/designer-1/goals.json +5 -0
- package/templates/members/designer-1/goals.md +3 -0
- package/templates/members/designer-1/skills/designer-1/SKILL.md +179 -0
- package/templates/members/designer-1/skills/designer-1/parts.md +55 -0
package/commands/doctor.js
CHANGED
|
@@ -175,34 +175,45 @@ function collectDoctor(options = {}) {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
function renderDoctor(payload) {
|
|
178
|
-
const lines = [];
|
|
179
|
-
lines.push('atris doctor');
|
|
180
|
-
lines.push('');
|
|
181
|
-
lines.push(`version: ${payload.version_line || payload.version}`);
|
|
182
|
-
lines.push(`node: ${payload.node.version} (${payload.node.exec_path || 'unknown'})`);
|
|
183
|
-
lines.push(`task support (22+): ${payload.task_support ? 'yes' : 'no'}`);
|
|
184
|
-
lines.push(`auth: ${payload.auth.signed_in ? `signed in as ${payload.auth.email || 'unknown'}` : 'signed out'}`);
|
|
185
|
-
lines.push(`workspace: ${payload.workspace_root}`);
|
|
186
|
-
lines.push(`cli runnable: ${payload.cli_runnable ? 'yes' : 'no'}`);
|
|
187
|
-
lines.push(`public bin: ${payload.bin.public_bin} (${payload.bin.shim ? `${payload.bin.shim_bytes} byte shim` : 'not a shim'})`);
|
|
188
178
|
const m = payload.members;
|
|
179
|
+
// Same rule as the boot screen: a folder is set up when it has an atris/ folder.
|
|
180
|
+
const setUp = fs.existsSync(path.join(payload.workspace_root, 'atris'));
|
|
181
|
+
const problems = [];
|
|
182
|
+
if (!payload.task_support) problems.push(`install node ${TASK_NODE_MAJOR} or newer (atris tasks need it)`);
|
|
183
|
+
if (!payload.cli_runnable) problems.push('npm install -g atris (reinstall atris; a program file is missing)');
|
|
184
|
+
if (!payload.bin.shim) problems.push('npm install -g atris (reinstall atris; its startup file is damaged)');
|
|
185
|
+
if (!payload.auth.signed_in) problems.push('atris login (sign in)');
|
|
186
|
+
if (!setUp) problems.push('atris init (set up this folder)');
|
|
187
|
+
if (setUp && m && !m.members) problems.push('atris init (add a team to this folder)');
|
|
188
|
+
if (m && !m.git_installed) problems.push('install git (the team works on safe copies with it)');
|
|
189
|
+
else if (m && !m.git_repo) problems.push('git init (let the team work on copies in this folder)');
|
|
190
|
+
if (m && !m.tools.length && !m.built_in_tool && payload.auth.signed_in) {
|
|
191
|
+
problems.push('install claude code or codex (the team needs one coding tool)');
|
|
192
|
+
}
|
|
193
|
+
const count = problems.length;
|
|
194
|
+
const lines = [count ? `atris doctor: ${count === 1 ? 'one thing needs' : `${count} things need`} fixing.` : 'atris doctor: this computer is ready.', ''];
|
|
195
|
+
const row = (label, value) => ` ${label.padEnd(14)}${value}`;
|
|
196
|
+
lines.push(row('atris', `v${payload.version}`));
|
|
197
|
+
lines.push(row('node', payload.task_support
|
|
198
|
+
? `${payload.node.version} (new enough; atris needs ${TASK_NODE_MAJOR} or newer)`
|
|
199
|
+
: `${payload.node.version} (too old; atris needs ${TASK_NODE_MAJOR} or newer; found at ${payload.node.exec_path || 'an unknown path'})`));
|
|
200
|
+
lines.push(row('signed in', payload.auth.signed_in ? (payload.auth.email || 'yes, account unknown') : 'no'));
|
|
201
|
+
lines.push(row('this folder', `${payload.workspace_root} (${setUp ? 'set up' : 'not set up'})`));
|
|
189
202
|
if (m) {
|
|
190
|
-
const
|
|
191
|
-
lines.push('');
|
|
192
|
-
lines.push(
|
|
193
|
-
lines.push(
|
|
194
|
-
lines.push(` git: ${!m.git_installed ? 'not installed' : (m.git_repo ? 'yes' : 'installed, but this folder is not a git repo')}`);
|
|
195
|
-
lines.push(` coding tool: ${toolNames.length ? toolNames.join(', ') : 'none'}`);
|
|
196
|
-
if (m.next) lines.push(` next: ${m.next}`);
|
|
203
|
+
const tools = m.tools.concat(m.built_in_tool ? ['built-in'] : []);
|
|
204
|
+
lines.push(row('team', m.members ? `${m.members} member${m.members === 1 ? '' : 's'}` : 'none'));
|
|
205
|
+
lines.push(row('git', !m.git_installed ? 'not installed' : (m.git_repo ? 'yes' : 'installed; this folder has no git history')));
|
|
206
|
+
lines.push(row('coding tools', tools.length ? tools.join(', ') : 'none'));
|
|
197
207
|
}
|
|
208
|
+
if (!payload.cli_runnable) lines.push(row('atris files', 'the program file is missing'));
|
|
209
|
+
if (!payload.bin.shim) lines.push(row('startup file', `${payload.bin.public_bin} is missing or unexpected (${payload.bin.shim_bytes === null ? 'size unknown' : `${payload.bin.shim_bytes} bytes`})`));
|
|
198
210
|
if (payload.warnings && payload.warnings.length) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
lines.push('');
|
|
204
|
-
lines.push(`install Node ${TASK_NODE_MAJOR}+ before running atris task.`);
|
|
211
|
+
for (const warning of payload.warnings) {
|
|
212
|
+
const plain = warning.replace('cli install is a dirty git checkout', 'the installed atris copy has unsaved changes').replace('cli install is a detached git checkout', 'the installed atris copy is not on a named branch');
|
|
213
|
+
lines.push(row('note', plain));
|
|
214
|
+
}
|
|
205
215
|
}
|
|
216
|
+
if (count) lines.push('', `fix: ${problems[0]}`);
|
|
206
217
|
return lines.join('\n');
|
|
207
218
|
}
|
|
208
219
|
|
|
@@ -210,9 +221,10 @@ function doctorCommand(args = [], options = {}) {
|
|
|
210
221
|
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
211
222
|
console.log('Usage: atris doctor [--json]');
|
|
212
223
|
console.log('');
|
|
213
|
-
console.log('
|
|
214
|
-
console.log('
|
|
215
|
-
console.log('
|
|
224
|
+
console.log('Checks that this computer is ready: a new enough node, a working atris install,');
|
|
225
|
+
console.log('whether you are signed in and whether this folder is set up.');
|
|
226
|
+
console.log('Also checks what the team needs: members, git and a coding tool.');
|
|
227
|
+
console.log('--json prints the same facts for scripts.');
|
|
216
228
|
return 0;
|
|
217
229
|
}
|
|
218
230
|
const payload = collectDoctor(options);
|
package/commands/init.js
CHANGED
|
@@ -344,16 +344,16 @@ function initAtris() {
|
|
|
344
344
|
console.log(`workspace files ready (${ready.workspace.size})`);
|
|
345
345
|
if (withTeam || ready.team.size > 0) {
|
|
346
346
|
console.log(`team members ready (${ready.team.size})`);
|
|
347
|
-
console.log('more members
|
|
347
|
+
console.log('add more members any time: atris member create <name>');
|
|
348
348
|
}
|
|
349
349
|
if (withAdapters || ready.adapters.size > 0) {
|
|
350
|
-
console.log(`
|
|
350
|
+
console.log(`set up for your coding tools (${ready.adapters.size})`);
|
|
351
351
|
}
|
|
352
352
|
if (withSkills || ready.skills.size > 0) {
|
|
353
353
|
console.log(`skills installed (${ready.skills.size})`);
|
|
354
354
|
}
|
|
355
355
|
if (minimal) {
|
|
356
|
-
console.log('
|
|
356
|
+
console.log('basic files ready (use --with-adapters or --with-skills to add more)');
|
|
357
357
|
}
|
|
358
358
|
};
|
|
359
359
|
// GUARD: Refuse nested init.
|
|
@@ -1172,7 +1172,7 @@ Read atris/MAP.md. Begin iteration 1.`;
|
|
|
1172
1172
|
if (saved.detail) console.log(` git said: ${saved.detail.split('\n')[0]}`);
|
|
1173
1173
|
}
|
|
1174
1174
|
}
|
|
1175
|
-
console.log('\natris
|
|
1175
|
+
console.log('\natris is set up in this folder.');
|
|
1176
1176
|
console.log('no commands to learn. tell your agent what you want in plain words.');
|
|
1177
1177
|
} else {
|
|
1178
1178
|
console.error('✗ Error: atris.md not found in package');
|
package/commands/join.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// atris join --name <name> [--business <id>] [--mission "<text>"]
|
|
4
|
+
// Makes this session a named agent that lives on a home computer
|
|
5
|
+
// (POST /join-as-agent). The agent key goes in the same slot `atris login
|
|
6
|
+
// --agent` uses, and the joined agent is remembered next to it so
|
|
7
|
+
// `atris night` and `atris morning` act for it by default.
|
|
8
|
+
// Without --mission, the agent's purpose line from this project's own
|
|
9
|
+
// atris/team/<name>/MISSION.md (its North Star) is shown and used as the mission.
|
|
10
|
+
//
|
|
11
|
+
// `atris join <invite-code>` (accepting an invite) still lives in
|
|
12
|
+
// commands/social.js; bin/atris.js routes here only when --name is given.
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { parseFlags, wantsHelp, resolveBusinessId, errorText, cleanText } = require('../lib/night-shift');
|
|
16
|
+
|
|
17
|
+
const NOT_LOGGED_IN = 'not logged in. run: atris login';
|
|
18
|
+
|
|
19
|
+
function showJoinAgentHelp(log = console.log) {
|
|
20
|
+
log('usage: atris join --name <name> [--business <id>] [--mission "<text>"]');
|
|
21
|
+
log('');
|
|
22
|
+
log('Make this session a named agent that lives on a computer. With --business');
|
|
23
|
+
log('it joins that business\'s computer; without it, your personal computer.');
|
|
24
|
+
log('Running it again with the same name gives back the same agent.');
|
|
25
|
+
log('');
|
|
26
|
+
log(' --name <name> The agent\'s name');
|
|
27
|
+
log(' --business <id> The business it belongs to (id or connected slug)');
|
|
28
|
+
log(' --mission "<text>" What the agent is for, in one line. Without it, the');
|
|
29
|
+
log(' North Star in atris/team/<name>/MISSION.md is used if there');
|
|
30
|
+
log(' is one. --mission "" sends no mission at all.');
|
|
31
|
+
log('');
|
|
32
|
+
log('To accept an invite instead: atris join <invite-code>');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// True when `atris join ...` means "join as an agent" rather than "accept an invite".
|
|
36
|
+
function isAgentJoin(args = []) {
|
|
37
|
+
return Array.isArray(args) && args.some((arg) => arg === '--name' || String(arg).startsWith('--name='));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function homeLabel(data, businessInput) {
|
|
41
|
+
const home = data && data.home;
|
|
42
|
+
if (home && typeof home === 'object') {
|
|
43
|
+
const name = home.business_name || home.name || home.label;
|
|
44
|
+
if (name) return String(name);
|
|
45
|
+
}
|
|
46
|
+
if (businessInput) return String(businessInput);
|
|
47
|
+
if (typeof home === 'string' && home.trim() && !/^(self|personal)$/i.test(home.trim())) return home.trim();
|
|
48
|
+
return 'your personal computer';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function tokenExpiry(token, decode) {
|
|
52
|
+
const claims = decode(token);
|
|
53
|
+
if (claims && typeof claims.exp === 'number') return new Date(claims.exp * 1000).toISOString();
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Store the agent key where `atris login --agent` keeps scoped keys, plus the
|
|
58
|
+
// joined agent itself. Returns a short note when it could not be saved.
|
|
59
|
+
function storeJoinedAgent(creds, token, joined, deps = {}) {
|
|
60
|
+
if (!creds || creds.source) {
|
|
61
|
+
// Env tokens, placed key files, and agent clients are not ours to rewrite.
|
|
62
|
+
return 'The agent key was not saved because this terminal signs in with a key from outside. Keep it in ATRIS_TOKEN to act as the agent.';
|
|
63
|
+
}
|
|
64
|
+
const authUtils = require('../utils/auth');
|
|
65
|
+
const decode = deps.decodeJwtClaims || authUtils.decodeJwtClaims;
|
|
66
|
+
const persist = deps.persistMintedAgentToken || require('./auth').persistMintedAgentToken;
|
|
67
|
+
const claims = decode(token) || {};
|
|
68
|
+
try {
|
|
69
|
+
persist({ ...creds, joined_agent: joined }, token, {
|
|
70
|
+
scopes: Array.isArray(claims.scopes) ? claims.scopes : [],
|
|
71
|
+
expiresAt: tokenExpiry(token, decode),
|
|
72
|
+
});
|
|
73
|
+
return null;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return `The agent key was not saved: ${String(error && error.message || error)}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const MAX_MISSION = 300;
|
|
80
|
+
|
|
81
|
+
// The one purpose line this project already keeps for an agent of this name:
|
|
82
|
+
// the North Star in atris/team/<name>/MISSION.md. Null unless it is a real
|
|
83
|
+
// line, not the placeholder a new member file starts with.
|
|
84
|
+
function purposeFromFolder(name, cwd) {
|
|
85
|
+
const slug = String(name).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
86
|
+
if (!slug) return null;
|
|
87
|
+
const memberDir = path.join(cwd, 'atris', 'team', slug);
|
|
88
|
+
const { missionPurpose } = require('../lib/member-core');
|
|
89
|
+
const purpose = missionPurpose({ memberDir, missionFile: path.join(memberDir, 'MISSION.md') });
|
|
90
|
+
const line = purpose.meaningful ? cleanText(purpose.northStar, MAX_MISSION) : '';
|
|
91
|
+
return line.length >= 8 ? { file: `atris/team/${slug}/MISSION.md`, line } : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function joinAgentCommand(args = [], deps = {}) {
|
|
95
|
+
const log = deps.log || console.log;
|
|
96
|
+
const err = deps.err || console.error;
|
|
97
|
+
if (wantsHelp(args)) {
|
|
98
|
+
showJoinAgentHelp(log);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { flags } = parseFlags(args, ['--name', '--business', '--mission'], []);
|
|
103
|
+
const name = String(flags.name || '').trim();
|
|
104
|
+
if (!name) {
|
|
105
|
+
err('Give the agent a name. Example: atris join --name scout');
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const load = deps.loadCredentials || require('../utils/auth').loadCredentials;
|
|
110
|
+
const request = deps.apiRequestJson || require('../utils/api').apiRequestJson;
|
|
111
|
+
const creds = await load(request);
|
|
112
|
+
const token = creds && typeof creds.token === 'string' ? creds.token.trim() : '';
|
|
113
|
+
if (!token) {
|
|
114
|
+
err(NOT_LOGGED_IN);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const businessId = flags.business ? resolveBusinessId(flags.business, deps) : null;
|
|
119
|
+
const body = { business_id: businessId, name };
|
|
120
|
+
let mission = String(flags.mission || '').trim();
|
|
121
|
+
if (flags.mission === undefined) {
|
|
122
|
+
const purpose = purposeFromFolder(name, deps.cwd || process.cwd());
|
|
123
|
+
if (purpose) {
|
|
124
|
+
mission = purpose.line;
|
|
125
|
+
log(`Using the purpose line in ${purpose.file} as the mission:`);
|
|
126
|
+
log(` "${purpose.line}"`);
|
|
127
|
+
log('Sending Atris only the name, the business and that one line, nothing else from your folder.');
|
|
128
|
+
log('To send your own instead: --mission "<text>". To send none: --mission "".');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (mission) body.mission = mission;
|
|
132
|
+
|
|
133
|
+
const result = await request('/join-as-agent', { method: 'POST', token, body, retries: 0 });
|
|
134
|
+
if (!result || !result.ok) {
|
|
135
|
+
err(`Could not join: ${errorText(result)}`);
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
const data = result.data || {};
|
|
139
|
+
if (!data.agent_id || !data.token) {
|
|
140
|
+
err('Atris did not return an agent and key, so nothing was joined.');
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const joined = {
|
|
145
|
+
agent_id: data.agent_id,
|
|
146
|
+
name,
|
|
147
|
+
business_id: businessId,
|
|
148
|
+
home: data.home == null ? null : data.home,
|
|
149
|
+
joined_at: deps.now ? deps.now() : new Date().toISOString(),
|
|
150
|
+
};
|
|
151
|
+
const note = storeJoinedAgent(creds, data.token, joined, deps);
|
|
152
|
+
|
|
153
|
+
log(`Joined ${homeLabel(data, flags.business)} as ${name}. This session now acts as that agent.`);
|
|
154
|
+
log(`Agent ${data.agent_id}.`);
|
|
155
|
+
if (note) err(note);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { joinAgentCommand, isAgentJoin, showJoinAgentHelp, storeJoinedAgent, purposeFromFolder };
|
package/commands/memory.js
CHANGED
|
@@ -21,8 +21,8 @@ const USAGE = [
|
|
|
21
21
|
...LIVE_HELP,
|
|
22
22
|
'copy to laptop: atris memory <pull|push|status> [folder] [--json]',
|
|
23
23
|
' pull copy your Atris memory folder down (default: ~/atris-memory)',
|
|
24
|
-
' push send back changes under wiki/ and
|
|
25
|
-
' --prune also removes pages under wiki/ and
|
|
24
|
+
' push send back changes under wiki/, files/ and features/ (any file under files/: photos, PDFs, recordings)',
|
|
25
|
+
' --prune also removes pages under wiki/, files/ and features/ that you deleted here',
|
|
26
26
|
' status show what changed here since the last pull',
|
|
27
27
|
].join('\n');
|
|
28
28
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// atris morning [--business <id>] [--agent <id>] [--json] [--full] [--no-save]
|
|
4
|
+
// Prints a short card for the last finished night shift (GET /morning) and
|
|
5
|
+
// saves what the agent learned into this project's atris/ folder.
|
|
6
|
+
|
|
7
|
+
const { parseFlags, wantsHelp, resolveTarget, errorText, cleanText, formatUsd } = require('../lib/night-shift');
|
|
8
|
+
const { nightFromMorning, saveLearned, LEARNED_REL, LESSONS_REL } = require('../lib/night-learned');
|
|
9
|
+
|
|
10
|
+
const NO_SHIFT = 'No night shift finished yet.';
|
|
11
|
+
const NOT_LOGGED_IN = 'not logged in. run: atris login';
|
|
12
|
+
|
|
13
|
+
function showMorningHelp(log = console.log) {
|
|
14
|
+
log('usage: atris morning [--business <id>] [--agent <id>] [--full] [--no-save] [--json]');
|
|
15
|
+
log('');
|
|
16
|
+
log('How your agent\'s last night went: the score, what it learned, and what it');
|
|
17
|
+
log('spent. What it learned is saved in this project\'s atris/ folder, in');
|
|
18
|
+
log(`${LEARNED_REL} (which a new session reads at start) and ${LESSONS_REL}.`);
|
|
19
|
+
log('Start a night with: atris night "<goal>"');
|
|
20
|
+
log('');
|
|
21
|
+
log(' --business <id> Receipt for this business (id or connected slug)');
|
|
22
|
+
log(' --agent <id> Receipt for this agent (default: the agent you joined as)');
|
|
23
|
+
log(' --full Also print the full receipt: what it read, made and met');
|
|
24
|
+
log(' --no-save Do not write anything into the atris/ folder');
|
|
25
|
+
log(' --json Print the raw receipt as JSON (saves nothing)');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function morningCommand(args = [], deps = {}) {
|
|
29
|
+
const log = deps.log || console.log;
|
|
30
|
+
const err = deps.err || console.error;
|
|
31
|
+
if (wantsHelp(args)) {
|
|
32
|
+
showMorningHelp(log);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { flags } = parseFlags(args, ['--business', '--agent'], ['--json', '--full', '--no-save']);
|
|
37
|
+
const load = deps.loadCredentials || require('../utils/auth').loadCredentials;
|
|
38
|
+
const request = deps.apiRequestJson || require('../utils/api').apiRequestJson;
|
|
39
|
+
const creds = await load(request);
|
|
40
|
+
const token = creds && typeof creds.token === 'string' ? creds.token.trim() : '';
|
|
41
|
+
if (!token) {
|
|
42
|
+
if (flags.json) log(JSON.stringify({ ok: false, error: NOT_LOGGED_IN }));
|
|
43
|
+
else err(NOT_LOGGED_IN);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { agentId, businessId } = resolveTarget(flags, creds, deps);
|
|
48
|
+
const query = new URLSearchParams();
|
|
49
|
+
if (businessId) query.set('business_id', businessId);
|
|
50
|
+
if (agentId) query.set('agent_id', agentId);
|
|
51
|
+
const qs = query.toString();
|
|
52
|
+
const result = await request(`/morning${qs ? `?${qs}` : ''}`, { method: 'GET', token });
|
|
53
|
+
|
|
54
|
+
const notFound = result && Number(result.status) === 404;
|
|
55
|
+
if (!notFound && (!result || !result.ok)) {
|
|
56
|
+
const message = errorText(result);
|
|
57
|
+
if (flags.json) log(JSON.stringify({ ok: false, error: message, status: result && result.status }));
|
|
58
|
+
else err(`Could not read the morning receipt: ${message}`);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data = (!notFound && result.data && typeof result.data === 'object') ? result.data : {};
|
|
63
|
+
const text = typeof data.text === 'string' ? data.text.replace(/\s+$/, '') : '';
|
|
64
|
+
|
|
65
|
+
if (flags.json) {
|
|
66
|
+
log(JSON.stringify(notFound ? { text: null, receipt: null } : data, null, 2));
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
const night = notFound ? null : nightFromMorning(data);
|
|
70
|
+
if (!night) {
|
|
71
|
+
log(text || NO_SHIFT);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let saved = null;
|
|
76
|
+
if (!flags['no-save']) saved = (deps.saveLearned || saveLearned)(deps.cwd || process.cwd(), night);
|
|
77
|
+
for (const line of renderCard(night, data, saved)) log(line);
|
|
78
|
+
if (flags.full && text) {
|
|
79
|
+
log('');
|
|
80
|
+
log(text);
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function headline(night) {
|
|
86
|
+
const goal = `Night on "${night.goal}"`;
|
|
87
|
+
if (night.score === null) return `${goal}: not scored.`;
|
|
88
|
+
const base = `${goal}: ${night.score} of 99`;
|
|
89
|
+
if (!night.known) return `${base}.`;
|
|
90
|
+
if (night.previous === null) return `${base}, first scored night.`;
|
|
91
|
+
// The server leaves the change out when the two nights were scored two
|
|
92
|
+
// different ways, so there is nothing fair to compare.
|
|
93
|
+
if (night.change === null) return `${base}.`;
|
|
94
|
+
if (night.change > 0) return `${base}, up from ${night.previous}.`;
|
|
95
|
+
if (night.change < 0) return `${base}, down from ${night.previous}.`;
|
|
96
|
+
return `${base}, the same as the night before.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function savedLine(night, saved) {
|
|
100
|
+
if (!saved) return 'Not saved to your folder (--no-save).';
|
|
101
|
+
if (saved.reason === 'no-folder') {
|
|
102
|
+
return 'Nothing saved: there is no atris/ folder here. Run this from your project to keep what it learned.';
|
|
103
|
+
}
|
|
104
|
+
if (saved.reason === 'error') return `Nothing saved: ${cleanText(saved.error, 120)}`;
|
|
105
|
+
if (!night.lines.length) return 'Nothing to save: the night learned nothing new.';
|
|
106
|
+
if (!saved.added) return `Nothing new to save: these notes are already in ${LEARNED_REL}.`;
|
|
107
|
+
const where = saved.lessons ? `${LEARNED_REL} and ${LESSONS_REL}` : LEARNED_REL;
|
|
108
|
+
return `Saved to your folder: ${saved.added} ${saved.added === 1 ? 'note' : 'notes'} (${where})`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderCard(night, data, saved) {
|
|
112
|
+
const out = [headline(night)];
|
|
113
|
+
if (night.lines.length) {
|
|
114
|
+
out.push('Learned:');
|
|
115
|
+
for (const text of night.lines.slice(0, 3)) out.push(`- ${text}`);
|
|
116
|
+
} else {
|
|
117
|
+
out.push('Learned: nothing new recorded.');
|
|
118
|
+
}
|
|
119
|
+
if (night.lessons.length) {
|
|
120
|
+
const shown = night.lessons.slice(0, 3).map((name) => `"${name}"`).join(', ');
|
|
121
|
+
const more = night.lessons.length > 3 ? `, and ${night.lessons.length - 3} more` : '';
|
|
122
|
+
out.push(`Used ${night.lessons.length} ${night.lessons.length === 1 ? 'lesson' : 'lessons'}: ${shown}${more}`);
|
|
123
|
+
}
|
|
124
|
+
const receipt = data.receipt || {};
|
|
125
|
+
if (typeof receipt.spent_usd === 'number' && typeof receipt.cap_usd === 'number') {
|
|
126
|
+
out.push(`Spent ${formatUsd(receipt.spent_usd)} of the ${formatUsd(receipt.cap_usd)} cap.`);
|
|
127
|
+
}
|
|
128
|
+
out.push(savedLine(night, saved));
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { morningCommand, showMorningHelp, renderCard, NO_SHIFT };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// atris night "<goal>" [--cap <usd>] [--business <id>] [--agent <id>] [--watch]
|
|
4
|
+
// Starts a night shift on the home computer (POST /night-shift). The shift
|
|
5
|
+
// stops on its own when it reaches the dollar cap.
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
parseFlags,
|
|
9
|
+
wantsHelp,
|
|
10
|
+
formatUsd,
|
|
11
|
+
resolveTarget,
|
|
12
|
+
errorText,
|
|
13
|
+
} = require('../lib/night-shift');
|
|
14
|
+
|
|
15
|
+
const DEFAULT_CAP_USD = 1;
|
|
16
|
+
const POLL_MS = 60 * 1000;
|
|
17
|
+
const FINAL_STATUSES = new Set(['capped', 'done', 'failed']);
|
|
18
|
+
const MAX_POLL_FAILURES = 5;
|
|
19
|
+
const NOT_LOGGED_IN = 'not logged in. run: atris login';
|
|
20
|
+
|
|
21
|
+
function showNightHelp(log = console.log) {
|
|
22
|
+
log('usage: atris night "<goal>" [--cap <usd>] [--business <id>] [--agent <id>] [--watch]');
|
|
23
|
+
log('');
|
|
24
|
+
log('Give your agent a goal for tonight. It works on its home computer and');
|
|
25
|
+
log('stops when it has spent the cap. Read what it did with: atris morning');
|
|
26
|
+
log('');
|
|
27
|
+
log(' --cap <usd> Most it may spend tonight, in dollars (default 1)');
|
|
28
|
+
log(' --business <id> Run for this business (id or connected slug)');
|
|
29
|
+
log(' --agent <id> Run as this agent (default: the agent you joined as)');
|
|
30
|
+
log(' --watch Stay open and print each step until the shift ends');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseCap(raw) {
|
|
34
|
+
if (raw === undefined) return { ok: true, cap: DEFAULT_CAP_USD };
|
|
35
|
+
const text = String(raw).trim().replace(/^\$/, '');
|
|
36
|
+
const cap = text === '' ? NaN : Number(text);
|
|
37
|
+
if (!Number.isFinite(cap) || cap <= 0) return { ok: false };
|
|
38
|
+
return { ok: true, cap };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatStart(startsAt, now = new Date()) {
|
|
42
|
+
const when = startsAt ? new Date(startsAt) : null;
|
|
43
|
+
if (!when || Number.isNaN(when.getTime())) return startsAt ? String(startsAt) : 'once the computer is free';
|
|
44
|
+
const time = when.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
45
|
+
const sameDay = when.toDateString() === now.toDateString();
|
|
46
|
+
if (sameDay) return time;
|
|
47
|
+
const day = when.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' });
|
|
48
|
+
return `${time} ${day}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shiftFrom(data) {
|
|
52
|
+
if (!data || typeof data !== 'object') return {};
|
|
53
|
+
return data.shift && typeof data.shift === 'object' ? data.shift : data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function eventsFrom(data) {
|
|
57
|
+
if (!data || typeof data !== 'object') return [];
|
|
58
|
+
if (Array.isArray(data.events)) return data.events;
|
|
59
|
+
const shift = shiftFrom(data);
|
|
60
|
+
return Array.isArray(shift.events) ? shift.events : [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function eventKey(event, index) {
|
|
64
|
+
if (event && typeof event === 'object' && (event.id || event.event_id)) return String(event.id || event.event_id);
|
|
65
|
+
return `#${index}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function eventLine(event) {
|
|
69
|
+
if (!event || typeof event !== 'object') return String(event);
|
|
70
|
+
const at = event.at || event.created_at || event.ts || event.timestamp;
|
|
71
|
+
const parsed = at ? new Date(at) : null;
|
|
72
|
+
const time = parsed && !Number.isNaN(parsed.getTime())
|
|
73
|
+
? parsed.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
|
74
|
+
: '';
|
|
75
|
+
const what = event.message || event.text || event.summary || event.detail || event.kind || event.type || 'step';
|
|
76
|
+
const spent = event.spent_usd != null ? ` (${formatUsd(event.spent_usd)} so far)` : '';
|
|
77
|
+
const line = `${String(what).replace(/\s+/g, ' ').trim()}${spent}`;
|
|
78
|
+
return time ? `${time} ${line}` : line;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function finalLine(shift) {
|
|
82
|
+
const status = String(shift.status || 'done');
|
|
83
|
+
const spent = formatUsd(shift.spent_usd || 0);
|
|
84
|
+
const cap = formatUsd(shift.cap_usd || 0);
|
|
85
|
+
if (status === 'capped') return `The shift stopped at its cap. Spent ${spent} of ${cap}. Read it with: atris morning`;
|
|
86
|
+
if (status === 'failed') return `The shift failed after spending ${spent} of ${cap}.`;
|
|
87
|
+
return `The shift is done. Spent ${spent} of ${cap}. Read it with: atris morning`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function watchShift(shiftId, ctx) {
|
|
91
|
+
const { request, token, log, err, sleep, pollMs } = ctx;
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
let failures = 0;
|
|
94
|
+
for (;;) {
|
|
95
|
+
const result = await request(`/night-shift/${encodeURIComponent(shiftId)}`, { method: 'GET', token });
|
|
96
|
+
if (!result || !result.ok) {
|
|
97
|
+
failures += 1;
|
|
98
|
+
err(`Could not check the shift: ${errorText(result)}`);
|
|
99
|
+
if (failures >= MAX_POLL_FAILURES) return 1;
|
|
100
|
+
} else {
|
|
101
|
+
failures = 0;
|
|
102
|
+
const events = eventsFrom(result.data);
|
|
103
|
+
events.forEach((event, index) => {
|
|
104
|
+
const key = eventKey(event, index);
|
|
105
|
+
if (seen.has(key)) return;
|
|
106
|
+
seen.add(key);
|
|
107
|
+
log(eventLine(event));
|
|
108
|
+
});
|
|
109
|
+
const shift = shiftFrom(result.data);
|
|
110
|
+
const status = String(shift.status || '').toLowerCase();
|
|
111
|
+
if (FINAL_STATUSES.has(status)) {
|
|
112
|
+
log(finalLine({ ...shift, status }));
|
|
113
|
+
return status === 'failed' ? 1 : 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await sleep(pollMs);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function nightCommand(args = [], deps = {}) {
|
|
121
|
+
const log = deps.log || console.log;
|
|
122
|
+
const err = deps.err || console.error;
|
|
123
|
+
if (wantsHelp(args)) {
|
|
124
|
+
showNightHelp(log);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { flags, positionals } = parseFlags(args, ['--cap', '--business', '--agent'], ['--watch']);
|
|
129
|
+
const goal = positionals.join(' ').trim();
|
|
130
|
+
if (!goal) {
|
|
131
|
+
err('Say what to learn tonight. Example: atris night "learn how our refunds work" --cap 1');
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
const capCheck = parseCap(flags.cap);
|
|
135
|
+
if (!capCheck.ok) {
|
|
136
|
+
err('The cap must be more than $0. Example: --cap 1');
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const load = deps.loadCredentials || require('../utils/auth').loadCredentials;
|
|
141
|
+
const request = deps.apiRequestJson || require('../utils/api').apiRequestJson;
|
|
142
|
+
const creds = await load(request);
|
|
143
|
+
const token = creds && typeof creds.token === 'string' ? creds.token.trim() : '';
|
|
144
|
+
if (!token) {
|
|
145
|
+
err(NOT_LOGGED_IN);
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { agentId, businessId } = resolveTarget(flags, creds, deps);
|
|
150
|
+
const body = { goal, cap_usd: capCheck.cap };
|
|
151
|
+
if (agentId) body.agent_id = agentId;
|
|
152
|
+
if (businessId) body.business_id = businessId;
|
|
153
|
+
|
|
154
|
+
const result = await request('/night-shift', { method: 'POST', token, body, retries: 0 });
|
|
155
|
+
if (!result || !result.ok) {
|
|
156
|
+
err(`Could not start the night shift: ${errorText(result)}`);
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
const data = result.data || {};
|
|
160
|
+
if (!data.shift_id) {
|
|
161
|
+
err('Atris did not return a shift id, so the shift may not have started.');
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
log(`Night shift starts at ${formatStart(data.starts_at, deps.now ? deps.now() : new Date())} with a ${formatUsd(capCheck.cap)} cap. Shift ${data.shift_id}.`);
|
|
166
|
+
|
|
167
|
+
if (!flags.watch) return 0;
|
|
168
|
+
return watchShift(data.shift_id, {
|
|
169
|
+
request,
|
|
170
|
+
token,
|
|
171
|
+
log,
|
|
172
|
+
err,
|
|
173
|
+
sleep: deps.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
174
|
+
pollMs: deps.pollMs != null ? deps.pollMs : POLL_MS,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = { nightCommand, showNightHelp, parseCap, formatStart, eventLine };
|
package/commands/recap.js
CHANGED
|
@@ -312,8 +312,8 @@ function renderRecapMinute(data, { person } = {}) {
|
|
|
312
312
|
if (shipped.length) {
|
|
313
313
|
const named = recapSoftTitle(shipped[0].title);
|
|
314
314
|
return named
|
|
315
|
-
? `${greet}you already
|
|
316
|
-
: `${greet}you already
|
|
315
|
+
? `${greet}you already finished ${named}.`
|
|
316
|
+
: `${greet}you already finished the last piece of work.`;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
if (inProgress.length) {
|
|
@@ -321,12 +321,12 @@ function renderRecapMinute(data, { person } = {}) {
|
|
|
321
321
|
const named = recapSoftTitle(item && item.title);
|
|
322
322
|
if (item && item.owner && named) {
|
|
323
323
|
return [
|
|
324
|
-
`${greet}${named}
|
|
324
|
+
`${greet}you are working on ${named}.`,
|
|
325
325
|
'',
|
|
326
326
|
`next: ${taskCommand({ display_id: item.id, status: 'claimed' })}`,
|
|
327
327
|
].join('\n');
|
|
328
328
|
}
|
|
329
|
-
if (named) return `${greet}${named} is ready to
|
|
329
|
+
if (named) return `${greet}${named} is ready to start.`;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
return `${greet}quiet window. nothing moved in this period.`;
|
package/commands/task.js
CHANGED
|
@@ -6340,13 +6340,42 @@ function cmdTag(args) {
|
|
|
6340
6340
|
if (!id) failTask('atris task tag', 'missing_id', 'task id required');
|
|
6341
6341
|
const add = collectFlagValues(args, '--add');
|
|
6342
6342
|
const remove = collectFlagValues(args, '--remove');
|
|
6343
|
-
|
|
6344
|
-
|
|
6343
|
+
const setTag = pos.slice(1).join(' ').trim();
|
|
6344
|
+
if (!add.length && !remove.length && !setTag) {
|
|
6345
|
+
failTask('atris task tag', 'missing_tags', 'a tag, or at least one --add <tag> or --remove <tag>, required');
|
|
6346
|
+
}
|
|
6347
|
+
if (setTag && (add.length || remove.length)) {
|
|
6348
|
+
failTask('atris task tag', 'tag_conflict', 'use either `task tag <id> <tag>` or --add/--remove, not both');
|
|
6345
6349
|
}
|
|
6346
6350
|
const actor = flag(args, '--as') || DEFAULT_OWNER;
|
|
6347
6351
|
const taskDb = getTaskDb();
|
|
6348
6352
|
const db = taskDb.open();
|
|
6349
6353
|
const taskId = requireTaskId(taskDb, db, id, 'atris task tag');
|
|
6354
|
+
if (setTag) {
|
|
6355
|
+
// Sets the primary tag, the same field `task add --tag` writes at creation.
|
|
6356
|
+
const set = taskDb.setTaskTag(db, { id: taskId, actor: String(actor), tag: setTag });
|
|
6357
|
+
if (!set.tagged && set.reason !== 'no_changes') {
|
|
6358
|
+
failTask('atris task tag', set.reason || 'tag_failed', `tag failed: ${set.reason || 'unknown'}`, 1);
|
|
6359
|
+
}
|
|
6360
|
+
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
6361
|
+
const task = compactTaskFromProjection(projection, taskId);
|
|
6362
|
+
if (wantsJson(args)) {
|
|
6363
|
+
printJson({
|
|
6364
|
+
ok: true,
|
|
6365
|
+
action: set.tagged ? 'tag_set' : 'unchanged',
|
|
6366
|
+
task_id: taskId,
|
|
6367
|
+
tag: set.tag,
|
|
6368
|
+
previous_tag: set.tagged ? set.previous_tag : set.tag,
|
|
6369
|
+
projection_path: outPath,
|
|
6370
|
+
task,
|
|
6371
|
+
});
|
|
6372
|
+
return;
|
|
6373
|
+
}
|
|
6374
|
+
console.log(set.tagged
|
|
6375
|
+
? `tagged ${taskRef(task)} tag ${set.previous_tag || 'none'} -> ${set.tag}`
|
|
6376
|
+
: `no change ${taskRef(task)} tag ${set.tag}`);
|
|
6377
|
+
return;
|
|
6378
|
+
}
|
|
6350
6379
|
const result = taskDb.tagTask(db, { id: taskId, actor: String(actor), add, remove });
|
|
6351
6380
|
if (!result.tagged) {
|
|
6352
6381
|
if (result.reason === 'no_changes') {
|