atris 3.34.0 → 3.35.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 +0 -2
- package/FOR_AGENTS.md +5 -3
- package/atris/skills/engines/SKILL.md +16 -7
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris.md +4 -2
- package/ax +475 -17
- package/bin/atris.js +197 -44
- package/commands/aeo.js +52 -0
- package/commands/autoland.js +313 -19
- package/commands/business.js +91 -8
- package/commands/codex-goal.js +26 -2
- package/commands/drive.js +187 -0
- package/commands/engine.js +73 -2
- package/commands/feed.js +202 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +262 -3
- package/commands/init.js +13 -1
- package/commands/integrations.js +39 -11
- package/commands/interview.js +143 -0
- package/commands/land.js +114 -13
- package/commands/lesson.js +112 -1
- package/commands/linear.js +38 -0
- package/commands/member.js +398 -42
- package/commands/mission.js +554 -64
- package/commands/now.js +25 -1
- package/commands/radar.js +259 -14
- package/commands/serve.js +54 -0
- package/commands/status.js +50 -5
- package/commands/stripe.js +38 -0
- package/commands/supabase.js +39 -0
- package/commands/task.js +935 -71
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -0
- package/commands/worktree.js +68 -10
- package/commands/write.js +399 -0
- package/lib/auto-accept-certified.js +70 -19
- package/lib/autoland.js +39 -3
- package/lib/fleet.js +250 -9
- package/lib/memory-view.js +14 -5
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/official-cli-integration.js +174 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/review-integrity.js +147 -0
- package/lib/runner-command.js +23 -0
- package/lib/task-db.js +220 -7
- package/lib/task-proof.js +20 -0
- package/lib/task-receipt.js +93 -0
- package/package.json +1 -1
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// atris drive — self-driving tick over the mission plane.
|
|
2
|
+
// One tick: run mission doctor, auto-execute the fixes it prescribes that are
|
|
3
|
+
// safe to run (stale ready receipts -> verify tick), and log everything that
|
|
4
|
+
// still needs a human as an explicit disengagement. The metric that matters:
|
|
5
|
+
// disengagements per tick, driven to zero.
|
|
6
|
+
//
|
|
7
|
+
// State: .atris/state/drive.jsonl (one line per tick, append-only).
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const BIN = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
14
|
+
|
|
15
|
+
function runAtris(args, cwd) {
|
|
16
|
+
const res = spawnSync(process.execPath, [BIN, ...args], {
|
|
17
|
+
cwd,
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
timeout: 120000,
|
|
20
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
21
|
+
});
|
|
22
|
+
return { code: res.status, stdout: res.stdout || '', stderr: res.stderr || '' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseJsonLoose(text) {
|
|
26
|
+
if (!text) return null;
|
|
27
|
+
const start = text.indexOf('{');
|
|
28
|
+
if (start === -1) return null;
|
|
29
|
+
try { return JSON.parse(text.slice(start)); } catch (err) { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function statePath(cwd) {
|
|
33
|
+
return path.join(cwd, '.atris', 'state', 'drive.jsonl');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function loadHistory(cwd, limit = 20) {
|
|
37
|
+
try {
|
|
38
|
+
const lines = fs.readFileSync(statePath(cwd), 'utf8').trim().split('\n');
|
|
39
|
+
return lines.slice(-limit).map((l) => { try { return JSON.parse(l); } catch (e) { return null; } }).filter(Boolean);
|
|
40
|
+
} catch (err) { return []; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function appendState(cwd, record) {
|
|
44
|
+
const file = statePath(cwd);
|
|
45
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
46
|
+
fs.appendFileSync(file, JSON.stringify(record) + '\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function driveCommand(argv) {
|
|
50
|
+
const cwd = process.cwd();
|
|
51
|
+
const json = argv.includes('--json');
|
|
52
|
+
const dryRun = argv.includes('--dry-run');
|
|
53
|
+
|
|
54
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
55
|
+
console.log('atris drive - one self-driving tick over the mission plane');
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(' atris drive [--dry-run] [--json] doctor -> auto-fix safe findings -> count disengagements');
|
|
58
|
+
console.log(' atris drive status [--json] disengagement trend from past ticks');
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(' Auto-handled: stale_ready_receipt (runs the verify tick, completes on pass).');
|
|
61
|
+
console.log(' Everything else = a disengagement: the system needed a human. Logged, counted, escalated.');
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (argv[0] === 'status') {
|
|
66
|
+
const history = loadHistory(cwd);
|
|
67
|
+
if (!history.length) { console.log('No drive ticks yet. Run: atris drive'); return 0; }
|
|
68
|
+
if (json) { console.log(JSON.stringify({ ok: true, ticks: history }, null, 2)); return 0; }
|
|
69
|
+
console.log('drive — disengagements per tick (goal: 0)\n');
|
|
70
|
+
for (const t of history) {
|
|
71
|
+
console.log(` ${t.at} fixed ${t.fixed}/${t.findings} disengagements ${t.disengagements}`);
|
|
72
|
+
}
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const doctor = runAtris(['mission', 'doctor', '--json'], cwd);
|
|
77
|
+
const report = parseJsonLoose(doctor.stdout);
|
|
78
|
+
if (!report || !Array.isArray(report.findings)) {
|
|
79
|
+
console.error('drive: mission doctor returned no parseable findings.');
|
|
80
|
+
if (doctor.stderr) console.error(doctor.stderr.slice(0, 500));
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const fixed = [];
|
|
85
|
+
const disengagements = [];
|
|
86
|
+
|
|
87
|
+
// Missions the doctor says have no verifier. A verify-tick physically cannot
|
|
88
|
+
// pass without one, so promising to verify-tick such a mission is a phantom
|
|
89
|
+
// fix that never sticks — count it as a wheel-grab instead.
|
|
90
|
+
const noVerifier = new Set(
|
|
91
|
+
report.findings.filter((f) => f.code === 'missing_verifier' && f.mission_id).map((f) => f.mission_id),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
for (const f of report.findings) {
|
|
95
|
+
if (f.code === 'stale_ready_receipt' && f.mission_id && noVerifier.has(f.mission_id)) {
|
|
96
|
+
disengagements.push({ ...pick(f), reason: 'ready_without_verifier' });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (f.code === 'stale_ready_receipt' && f.mission_id && !dryRun) {
|
|
100
|
+
const tick = runAtris(['mission', 'tick', f.mission_id, '--verify', '--complete-on-pass', '--json'], cwd);
|
|
101
|
+
const result = parseJsonLoose(tick.stdout);
|
|
102
|
+
const passed = tick.code === 0 && result && result.ok !== false;
|
|
103
|
+
if (passed) {
|
|
104
|
+
fixed.push({ mission_id: f.mission_id, action: 'verify_tick', objective: f.objective });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
disengagements.push({ ...pick(f), reason: 'verify_tick_failed' });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (f.code === 'stale_ready_receipt' && dryRun) {
|
|
111
|
+
fixed.push({ mission_id: f.mission_id, action: 'would_verify_tick', objective: f.objective });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// No verifier and stale (planning >24h old, or already paused): park it.
|
|
115
|
+
// A paused mission is reversible; a zombie in planning is a permanent disengagement.
|
|
116
|
+
if (f.code === 'missing_verifier' && f.mission_id && isParkable(f)) {
|
|
117
|
+
if (dryRun) {
|
|
118
|
+
fixed.push({ mission_id: f.mission_id, action: 'would_park', objective: f.objective });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const stop = runAtris(['mission', 'stop', f.mission_id, '--pause', '--reason', 'drive: no verifier + stale, auto-parked (restart with a --verify to resume)'], cwd);
|
|
122
|
+
if (stop.code === 0) {
|
|
123
|
+
fixed.push({ mission_id: f.mission_id, action: 'parked', objective: f.objective });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
disengagements.push({ ...pick(f), reason: 'park_failed' });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
disengagements.push({ ...pick(f), reason: f.code });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// One stuck mission is one wheel-grab, even if it trips several findings.
|
|
133
|
+
// Count disengagements per mission so the metric tracks "how many missions
|
|
134
|
+
// still need a human", not "how many symptoms did we print".
|
|
135
|
+
const dedupedDisengagements = [];
|
|
136
|
+
const seenDisengaged = new Set();
|
|
137
|
+
for (const d of disengagements) {
|
|
138
|
+
const key = d.mission_id || `${d.reason}:${d.objective || ''}`;
|
|
139
|
+
if (seenDisengaged.has(key)) continue;
|
|
140
|
+
seenDisengaged.add(key);
|
|
141
|
+
dedupedDisengagements.push(d);
|
|
142
|
+
}
|
|
143
|
+
disengagements.length = 0;
|
|
144
|
+
disengagements.push(...dedupedDisengagements);
|
|
145
|
+
|
|
146
|
+
function isParkable(f) {
|
|
147
|
+
if (f.status === 'paused') return true;
|
|
148
|
+
if (f.status !== 'planning') return false;
|
|
149
|
+
const m = /^mission-(\d{4}-\d{2}-\d{2})-/.exec(f.mission_id || '');
|
|
150
|
+
if (!m) return false;
|
|
151
|
+
return (Date.now() - new Date(m[1] + 'T00:00:00Z').getTime()) > 24 * 3600 * 1000;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function pick(f) {
|
|
155
|
+
return { mission_id: f.mission_id, code: f.code, owner: f.owner, objective: (f.objective || '').slice(0, 120), next: f.next };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const record = {
|
|
159
|
+
at: new Date().toISOString(),
|
|
160
|
+
findings: report.findings.length,
|
|
161
|
+
checked: report.checked_count,
|
|
162
|
+
fixed: fixed.length,
|
|
163
|
+
disengagements: disengagements.length,
|
|
164
|
+
dry_run: dryRun,
|
|
165
|
+
fixed_detail: fixed,
|
|
166
|
+
disengagement_detail: disengagements,
|
|
167
|
+
};
|
|
168
|
+
if (!dryRun) appendState(cwd, record);
|
|
169
|
+
|
|
170
|
+
if (json) { console.log(JSON.stringify({ ok: true, ...record }, null, 2)); return 0; }
|
|
171
|
+
|
|
172
|
+
console.log(`drive tick — ${report.checked_count} missions checked, ${report.findings.length} findings`);
|
|
173
|
+
console.log(` auto-fixed: ${fixed.length}`);
|
|
174
|
+
for (const x of fixed) console.log(` ✓ ${x.action} ${x.mission_id}`);
|
|
175
|
+
console.log(` disengagements: ${disengagements.length} (a human has to grab the wheel)`);
|
|
176
|
+
const byCode = {};
|
|
177
|
+
for (const d of disengagements) byCode[d.reason] = (byCode[d.reason] || 0) + 1;
|
|
178
|
+
for (const [code, n] of Object.entries(byCode)) console.log(` ✋ ${code} ×${n}`);
|
|
179
|
+
const history = loadHistory(cwd, 5);
|
|
180
|
+
if (history.length > 1) {
|
|
181
|
+
console.log(` trend: ${history.map((t) => t.disengagements).join(' → ')}`);
|
|
182
|
+
}
|
|
183
|
+
console.log(` next: atris drive status · fix the ✋ list · re-run atris drive`);
|
|
184
|
+
return disengagements.length > 0 ? 0 : 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = { driveCommand };
|
package/commands/engine.js
CHANGED
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
RUNNER_PROFILE_NAMES,
|
|
24
24
|
buildRunnerCommand,
|
|
25
25
|
} = require('../lib/runner-command');
|
|
26
|
+
const { FLEET_CAPABLE, runDispatchFlight } = require('../lib/fleet');
|
|
26
27
|
|
|
27
28
|
const HOUSE_ENGINE = 'atris-fast';
|
|
28
29
|
|
|
@@ -240,11 +241,79 @@ function runEngineTest(targets, { json, root } = {}) {
|
|
|
240
241
|
return failures.length ? 1 : 0;
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
// One-command dispatch: claim -> worktree start -> bounded prompt -> engine
|
|
245
|
+
// -> re-run Check: -> ship -> task ready, in one call instead of the 6
|
|
246
|
+
// hand-rolled Bash calls per task the manual version took. Task ids are
|
|
247
|
+
// positional; --engine/--prompt-file are the only flags, so parse by hand
|
|
248
|
+
// instead of the generic "anything starting with --" split used above (that
|
|
249
|
+
// split would swallow an --engine value like "cursor" as a task id).
|
|
250
|
+
function parseDispatchArgs(args) {
|
|
251
|
+
const taskIds = [];
|
|
252
|
+
let engine = '';
|
|
253
|
+
let promptFile = '';
|
|
254
|
+
let base = '';
|
|
255
|
+
let json = false;
|
|
256
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
257
|
+
const a = args[i];
|
|
258
|
+
if (a === '--engine') { engine = args[i + 1] || ''; i += 1; continue; }
|
|
259
|
+
if (a.startsWith('--engine=')) { engine = a.slice('--engine='.length); continue; }
|
|
260
|
+
if (a === '--prompt-file') { promptFile = args[i + 1] || ''; i += 1; continue; }
|
|
261
|
+
if (a.startsWith('--prompt-file=')) { promptFile = a.slice('--prompt-file='.length); continue; }
|
|
262
|
+
if (a === '--base') { base = args[i + 1] || ''; i += 1; continue; }
|
|
263
|
+
if (a.startsWith('--base=')) { base = a.slice('--base='.length); continue; }
|
|
264
|
+
if (a === '--json') { json = true; continue; }
|
|
265
|
+
if (a.startsWith('--')) continue;
|
|
266
|
+
taskIds.push(a);
|
|
267
|
+
}
|
|
268
|
+
return { taskIds, engine, promptFile, base, json };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function runDispatchCommand(args, root) {
|
|
272
|
+
const { taskIds, engine, promptFile, base, json } = parseDispatchArgs(args);
|
|
273
|
+
if (!taskIds.length || !engine) {
|
|
274
|
+
console.error('usage: atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>]');
|
|
275
|
+
return 2;
|
|
276
|
+
}
|
|
277
|
+
const canonical = canonicalEngineName(engine);
|
|
278
|
+
if (!canonical || !FLEET_CAPABLE.includes(canonical)) {
|
|
279
|
+
console.error(`engine dispatch: --engine must be one of ${FLEET_CAPABLE.join(', ')}`);
|
|
280
|
+
return 2;
|
|
281
|
+
}
|
|
282
|
+
// Argument-shape errors surface before environment errors: --prompt-file
|
|
283
|
+
// with multiple ids is wrong on any machine, installed CLI or not.
|
|
284
|
+
let promptOverride = '';
|
|
285
|
+
if (promptFile) {
|
|
286
|
+
if (taskIds.length > 1) {
|
|
287
|
+
console.error('engine dispatch: --prompt-file only supports a single task id');
|
|
288
|
+
return 2;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
promptOverride = fs.readFileSync(promptFile, 'utf8');
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.error(`engine dispatch: could not read --prompt-file ${promptFile}: ${err.message}`);
|
|
294
|
+
return 2;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const def = RUNNER_PROFILE_DEFS[canonical];
|
|
298
|
+
if (!binInstalled(def.bin)) {
|
|
299
|
+
console.error(`engine dispatch: ${canonical} CLI (${def.bin}) is not installed here`);
|
|
300
|
+
return 2;
|
|
301
|
+
}
|
|
302
|
+
return runDispatchFlight({ root, taskIds, engine: canonical, prompt: promptOverride, ...(base ? { checkoutBase: base } : {}) }).then((flight) => {
|
|
303
|
+
if (json) console.log(JSON.stringify(flight, null, 2));
|
|
304
|
+
return flight.paused.length ? 1 : 0;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
243
308
|
function engineCommand(args = []) {
|
|
309
|
+
const root = process.cwd();
|
|
310
|
+
if ((args[0] || '').trim() === 'dispatch') {
|
|
311
|
+
return runDispatchCommand(args.slice(1), root);
|
|
312
|
+
}
|
|
313
|
+
|
|
244
314
|
const json = args.includes('--json');
|
|
245
315
|
const positional = args.filter((a) => !a.startsWith('--'));
|
|
246
316
|
const sub = (positional[0] || '').trim();
|
|
247
|
-
const root = process.cwd();
|
|
248
317
|
|
|
249
318
|
if (sub === 'test') {
|
|
250
319
|
return runEngineTest(positional.slice(1), { json, root });
|
|
@@ -269,7 +338,7 @@ function engineCommand(args = []) {
|
|
|
269
338
|
}
|
|
270
339
|
|
|
271
340
|
if (sub === 'help') {
|
|
272
|
-
console.log('\n atris engine roster + current default\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
341
|
+
console.log('\n atris engine roster + current default\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>]\n one-command claim, worktree, build, verify, ship, ready\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
273
342
|
return 0;
|
|
274
343
|
}
|
|
275
344
|
|
|
@@ -295,5 +364,7 @@ module.exports = {
|
|
|
295
364
|
roster,
|
|
296
365
|
probeEngine,
|
|
297
366
|
runEngineTest,
|
|
367
|
+
parseDispatchArgs,
|
|
368
|
+
runDispatchCommand,
|
|
298
369
|
HOUSE_ENGINE,
|
|
299
370
|
};
|
package/commands/feed.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Feed: the business group feed from the terminal — read receipts, post receipts.
|
|
4
|
+
// Auth: existing atris login. Business: nearest .atris/business.json above cwd.
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { ensureValidCredentials } = require('../utils/auth');
|
|
9
|
+
const { apiRequestJson } = require('../utils/api');
|
|
10
|
+
|
|
11
|
+
function showHelp() {
|
|
12
|
+
console.log('');
|
|
13
|
+
console.log('Usage: atris feed [list] [n] [--author <name>] [--since <7d|24h>] [--full] [--json]');
|
|
14
|
+
console.log(' atris feed post "<content>"');
|
|
15
|
+
console.log('');
|
|
16
|
+
console.log('Description:');
|
|
17
|
+
console.log(' Read and write the business group feed (the same feed as the web GM dashboard).');
|
|
18
|
+
console.log(' Posts should be receipts and state changes only: what shipped, what blocked, what changed.');
|
|
19
|
+
console.log('');
|
|
20
|
+
console.log('Options:');
|
|
21
|
+
console.log(' --author <name> Only posts whose author matches (name, email prefix, or id prefix).');
|
|
22
|
+
console.log(' --since <window> Only posts newer than e.g. 7d, 48h.');
|
|
23
|
+
console.log(' --full Print full post bodies instead of one line each.');
|
|
24
|
+
console.log(' --json Machine-readable output.');
|
|
25
|
+
console.log(' --help, -h Show this help.');
|
|
26
|
+
console.log('');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findBusiness(startDir) {
|
|
30
|
+
let dir = startDir;
|
|
31
|
+
while (dir !== path.dirname(dir)) {
|
|
32
|
+
const file = path.join(dir, '.atris', 'business.json');
|
|
33
|
+
if (fs.existsSync(file)) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
36
|
+
if (parsed.business_id) return { businessId: parsed.business_id, name: parsed.name || parsed.slug, root: dir };
|
|
37
|
+
} catch { /* fall through to parent dirs */ }
|
|
38
|
+
}
|
|
39
|
+
dir = path.dirname(dir);
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadAuthorAliases(workspaceRoot) {
|
|
45
|
+
const file = path.join(workspaceRoot, '.atris', 'feed_authors.json');
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseSince(raw) {
|
|
54
|
+
const match = /^(\d+)([dh])$/.exec(String(raw || '').trim());
|
|
55
|
+
if (!match) return null;
|
|
56
|
+
const amount = parseInt(match[1], 10);
|
|
57
|
+
const ms = match[2] === 'd' ? amount * 24 * 3600 * 1000 : amount * 3600 * 1000;
|
|
58
|
+
return Date.now() - ms;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function authorLabel(post, aliases, selfId, selfEmail) {
|
|
62
|
+
if (post.post_type === 'agent_post') return post.agent_name || post.author_display_name || 'agent';
|
|
63
|
+
if (post.author_display_name) return post.author_display_name;
|
|
64
|
+
const id = post.author_id || '';
|
|
65
|
+
if (aliases[id]) return aliases[id];
|
|
66
|
+
if (selfId && id === selfId) return (selfEmail || 'me').split('@')[0];
|
|
67
|
+
return id ? id.slice(0, 8) : '?';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function fetchPosts(businessId, token, want) {
|
|
71
|
+
const pageSize = 100;
|
|
72
|
+
let all = [];
|
|
73
|
+
for (let offset = 0; all.length < want; offset += pageSize) {
|
|
74
|
+
const res = await apiRequestJson(
|
|
75
|
+
`/business/${businessId}/feed?limit=${pageSize}&offset=${offset}`,
|
|
76
|
+
{ token, timeoutMs: 20000 }
|
|
77
|
+
);
|
|
78
|
+
const statusCode = res.statusCode || res.status;
|
|
79
|
+
if (statusCode && statusCode >= 400) {
|
|
80
|
+
throw new Error(`Feed fetch failed (${statusCode}): ${JSON.stringify(res.data)}`);
|
|
81
|
+
}
|
|
82
|
+
const page = Array.isArray(res.data?.posts) ? res.data.posts : Array.isArray(res.data) ? res.data : [];
|
|
83
|
+
all = all.concat(page);
|
|
84
|
+
if (page.length < pageSize) break;
|
|
85
|
+
}
|
|
86
|
+
return all;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function feedCommand(args = []) {
|
|
90
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
91
|
+
showHelp();
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const business = findBusiness(process.cwd());
|
|
96
|
+
if (!business) {
|
|
97
|
+
console.error('✗ No .atris/business.json found above this directory. Run from a bound workspace.');
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const auth = await ensureValidCredentials(apiRequestJson);
|
|
102
|
+
if (auth.error || !auth.credentials?.token) {
|
|
103
|
+
console.error(`✗ Not logged in (${auth.error || 'no token'}). Run: atris login`);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
const token = auth.credentials.token;
|
|
107
|
+
const selfId = auth.credentials.user_id || auth.user?.id || null;
|
|
108
|
+
const selfEmail = auth.credentials.email || auth.user?.email || null;
|
|
109
|
+
|
|
110
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
111
|
+
const sub = positional[0] || 'list';
|
|
112
|
+
|
|
113
|
+
if (sub === 'post') {
|
|
114
|
+
const content = positional.slice(1).join(' ').trim();
|
|
115
|
+
if (!content) {
|
|
116
|
+
console.error('✗ Nothing to post. Usage: atris feed post "<content>"');
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
const res = await apiRequestJson(`/business/${business.businessId}/feed`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
token,
|
|
122
|
+
body: { content: content.slice(0, 10000) },
|
|
123
|
+
timeoutMs: 20000,
|
|
124
|
+
});
|
|
125
|
+
if (!res.data?.id) {
|
|
126
|
+
console.error(`✗ Post failed: ${JSON.stringify(res.data)}`);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
console.log(`✓ Posted to ${business.name || 'business'} feed (${res.data.id})`);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (sub !== 'list') {
|
|
134
|
+
console.error(`✗ Unknown subcommand: ${sub}`);
|
|
135
|
+
showHelp();
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const countArg = parseInt(positional[1], 10);
|
|
140
|
+
const count = Number.isFinite(countArg) ? Math.max(1, Math.min(300, countArg)) : 15;
|
|
141
|
+
const authorFlag = args.includes('--author') ? String(args[args.indexOf('--author') + 1] || '').toLowerCase() : null;
|
|
142
|
+
const sinceFlag = args.includes('--since') ? parseSince(args[args.indexOf('--since') + 1]) : null;
|
|
143
|
+
if (args.includes('--since') && sinceFlag === null) {
|
|
144
|
+
console.error('✗ Bad --since window. Use forms like 7d or 48h.');
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
const full = args.includes('--full');
|
|
148
|
+
const json = args.includes('--json');
|
|
149
|
+
|
|
150
|
+
const aliases = loadAuthorAliases(business.root);
|
|
151
|
+
// Over-fetch when filtering so the filtered result can still reach `count`.
|
|
152
|
+
const fetchWant = authorFlag || sinceFlag ? 300 : count;
|
|
153
|
+
let posts = await fetchPosts(business.businessId, token, fetchWant);
|
|
154
|
+
|
|
155
|
+
if (sinceFlag) posts = posts.filter((p) => Date.parse(p.created_at) >= sinceFlag);
|
|
156
|
+
if (authorFlag) {
|
|
157
|
+
posts = posts.filter((p) => {
|
|
158
|
+
const label = authorLabel(p, aliases, selfId, selfEmail).toLowerCase();
|
|
159
|
+
return label.includes(authorFlag) || String(p.author_id || '').startsWith(authorFlag);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
posts = posts.slice(0, count);
|
|
163
|
+
|
|
164
|
+
if (json) {
|
|
165
|
+
console.log(JSON.stringify(posts.map((p) => ({
|
|
166
|
+
id: p.id,
|
|
167
|
+
author: authorLabel(p, aliases, selfId, selfEmail),
|
|
168
|
+
type: p.post_type,
|
|
169
|
+
created_at: p.created_at,
|
|
170
|
+
content: p.content,
|
|
171
|
+
})), null, 2));
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!posts.length) {
|
|
176
|
+
console.log('No posts matched.');
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const p of posts) {
|
|
181
|
+
const when = String(p.created_at || '').slice(0, 16).replace('T', ' ');
|
|
182
|
+
const who = authorLabel(p, aliases, selfId, selfEmail);
|
|
183
|
+
if (full) {
|
|
184
|
+
console.log(`─── ${who} — ${when}`);
|
|
185
|
+
console.log(String(p.content || '').trim());
|
|
186
|
+
console.log('');
|
|
187
|
+
} else {
|
|
188
|
+
const oneLine = String(p.content || '').replace(/\s+/g, ' ').slice(0, 120);
|
|
189
|
+
console.log(`[${when}] ${who}: ${oneLine}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
feedCommand,
|
|
197
|
+
findBusiness,
|
|
198
|
+
loadAuthorAliases,
|
|
199
|
+
parseSince,
|
|
200
|
+
authorLabel,
|
|
201
|
+
fetchPosts,
|
|
202
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { createOfficialCliCommand } = require('../lib/official-cli-integration');
|
|
2
|
+
|
|
3
|
+
const githubCommand = createOfficialCliCommand({
|
|
4
|
+
name: 'github',
|
|
5
|
+
binary: 'gh',
|
|
6
|
+
versionArgs: ['--version'],
|
|
7
|
+
authArgs: ['auth', 'status'],
|
|
8
|
+
installHint: 'https://cli.github.com/',
|
|
9
|
+
loginHint: 'gh auth login',
|
|
10
|
+
commands: [
|
|
11
|
+
{
|
|
12
|
+
usage: 'pr list',
|
|
13
|
+
match: ['pr', 'list'],
|
|
14
|
+
forward: ['pr', 'list'],
|
|
15
|
+
description: 'list pull requests',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
usage: 'pr create',
|
|
19
|
+
match: ['pr', 'create'],
|
|
20
|
+
forward: ['pr', 'create'],
|
|
21
|
+
description: 'create a pull request',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
usage: 'pr checks',
|
|
25
|
+
match: ['pr', 'checks'],
|
|
26
|
+
forward: ['pr', 'checks'],
|
|
27
|
+
description: 'show pull request checks',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
usage: 'pr view',
|
|
31
|
+
match: ['pr', 'view'],
|
|
32
|
+
forward: ['pr', 'view'],
|
|
33
|
+
description: 'show pull request details',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
module.exports = { githubCommand };
|