atris 3.56.0 → 3.56.2
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/atris/skills/engines/SKILL.md +11 -3
- package/atris/skills/x-search/SKILL.md +20 -1
- package/atris/skills/youtube/SKILL.md +51 -43
- package/bin/atris.js +134 -90
- package/commands/auth.js +6 -1
- package/commands/autopilot-front.js +29 -13
- package/commands/brainstorm.js +77 -476
- package/commands/business.js +13 -1
- package/commands/engine.js +7 -0
- package/commands/experiments.js +178 -0
- package/commands/fleet-report.js +2 -2
- package/commands/founder.js +12 -0
- package/commands/human-missions.js +64 -2
- package/commands/init.js +2 -35
- package/commands/integrations.js +100 -0
- package/commands/land.js +53 -23
- package/commands/later.js +52 -0
- package/commands/launchpad.js +45 -10
- package/commands/log.js +79 -30
- package/commands/mission.js +117 -22
- package/commands/next.js +153 -63
- package/commands/now.js +115 -38
- package/commands/recap.js +97 -4
- package/commands/run-front.js +20 -5
- package/commands/spaceship.js +52 -12
- package/commands/status.js +38 -7
- package/commands/task.js +56 -19
- package/commands/terminal.js +5 -5
- package/commands/workflow.js +19 -5
- package/commands/x-search.js +123 -13
- package/commands/youtube.js +941 -36
- package/lib/account-bound.js +7 -0
- package/lib/apply-gate.js +102 -0
- package/lib/config-guard.js +106 -0
- package/lib/context-gatherer.js +55 -8
- package/lib/engine-ask.js +16 -6
- package/lib/engine-registry.js +14 -4
- package/lib/first-minute.js +571 -26
- package/lib/known-commands.js +1 -1
- package/lib/pack-capabilities.js +13 -3
- package/lib/runner-command.js +5 -3
- package/lib/scratch-root.js +44 -0
- package/lib/workspace-scaffold.js +3 -1
- package/package.json +1 -1
- package/utils/auth.js +84 -6
package/commands/spaceship.js
CHANGED
|
@@ -13,17 +13,42 @@
|
|
|
13
13
|
* atris spaceship --hours 4 --repo /path/to/repo --interval 780 --yes
|
|
14
14
|
* atris spaceship --hours 0.01 --tick-cmd /tmp/stub.sh --no-email --yes
|
|
15
15
|
*
|
|
16
|
-
* Without --yes, spaceship only
|
|
16
|
+
* Without --yes, spaceship only talks the keep-working plan (no email, no run).
|
|
17
17
|
* `--json` without --yes prints a JSON refuse and still does not start.
|
|
18
|
+
* `--yes` from an unbound scratch folder refuses: that folder is not a room.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
const path = require('path');
|
|
21
22
|
const fs = require('fs');
|
|
22
23
|
const { spawn } = require('child_process');
|
|
23
24
|
const { hasYesFlag, argsWantHelp, wantsJson } = require('../lib/noninteractive');
|
|
25
|
+
const { isUnboundScratchFolder, refuseUnboundScratch } = require('../lib/scratch-root');
|
|
26
|
+
const { resolveWorkspaceRoot } = require('../lib/mission-root');
|
|
27
|
+
const { personName } = require('../lib/first-minute');
|
|
24
28
|
|
|
25
29
|
const SCRIPT = path.join(__dirname, '..', 'scripts', 'spaceship.sh');
|
|
26
30
|
|
|
31
|
+
function greet(person) {
|
|
32
|
+
return person ? `hey ${person}, ` : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readFlagValue(args, name) {
|
|
36
|
+
const list = Array.isArray(args) ? args : [];
|
|
37
|
+
const idx = list.indexOf(name);
|
|
38
|
+
const raw = idx !== -1 && list[idx + 1] && !String(list[idx + 1]).startsWith('-')
|
|
39
|
+
? String(list[idx + 1]).trim()
|
|
40
|
+
: '';
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function spokenHours(raw) {
|
|
45
|
+
const text = raw === undefined || raw === null ? '4' : String(raw).trim();
|
|
46
|
+
const n = Number(text);
|
|
47
|
+
if (!Number.isFinite(n) || n <= 0) return '4 hours';
|
|
48
|
+
if (n === 1) return `${text} hour`;
|
|
49
|
+
return `${text} hours`;
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
function printUsage() {
|
|
28
53
|
const lines = [
|
|
29
54
|
'Usage: atris spaceship [--hours N] [--interval SEC] [--repo PATH]',
|
|
@@ -31,25 +56,34 @@ function printUsage() {
|
|
|
31
56
|
' [--idle-alert N] [--halt-alert N] [--label NAME]',
|
|
32
57
|
' [--no-email] [--yes]',
|
|
33
58
|
'',
|
|
34
|
-
|
|
35
|
-
'Without --yes,
|
|
36
|
-
'Defaults: --hours 4, --interval 780, tick = atris autopilot --auto --iterations=1',
|
|
59
|
+
"Keep working here for a few hours. I'll write you if something changes.",
|
|
60
|
+
'Without --yes, I only say the plan. --no-email stays quiet.',
|
|
37
61
|
];
|
|
38
62
|
console.log(lines.join('\n'));
|
|
39
63
|
}
|
|
40
64
|
|
|
41
65
|
function planLines(args = []) {
|
|
42
|
-
const
|
|
43
|
-
const hours =
|
|
66
|
+
const list = Array.isArray(args) ? args : [];
|
|
67
|
+
const hours = spokenHours(readFlagValue(list, '--hours') || '4');
|
|
68
|
+
const next = list.includes('--no-email')
|
|
69
|
+
? 'next: atris spaceship --yes'
|
|
70
|
+
: "I'll write you if something changes. next: atris spaceship --yes";
|
|
44
71
|
return [
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
' tick: atris autopilot --auto --iterations=1',
|
|
49
|
-
'Pass --yes to start the overnight run.',
|
|
72
|
+
`${greet(personName())}I can keep working here for ${hours}.`,
|
|
73
|
+
'',
|
|
74
|
+
next,
|
|
50
75
|
];
|
|
51
76
|
}
|
|
52
77
|
|
|
78
|
+
function spaceshipTargetRoot(args = []) {
|
|
79
|
+
const list = Array.isArray(args) ? args : [];
|
|
80
|
+
const idx = list.indexOf('--repo');
|
|
81
|
+
const raw = idx !== -1 && list[idx + 1] && !String(list[idx + 1]).startsWith('-')
|
|
82
|
+
? path.resolve(list[idx + 1])
|
|
83
|
+
: process.cwd();
|
|
84
|
+
return resolveWorkspaceRoot(raw);
|
|
85
|
+
}
|
|
86
|
+
|
|
53
87
|
function spaceship(args = []) {
|
|
54
88
|
const list = Array.isArray(args) ? args : [];
|
|
55
89
|
if (argsWantHelp(list) || list.includes('--help') || list.includes('-h')) {
|
|
@@ -75,6 +109,12 @@ function spaceship(args = []) {
|
|
|
75
109
|
process.exit(2);
|
|
76
110
|
}
|
|
77
111
|
|
|
112
|
+
// --yes starts the run. It is not a workspace unlock. An unbound
|
|
113
|
+
// scratch folder is not a room (same class as slack/gmail from scratch).
|
|
114
|
+
if (isUnboundScratchFolder(spaceshipTargetRoot(list))) {
|
|
115
|
+
process.exit(refuseUnboundScratch());
|
|
116
|
+
}
|
|
117
|
+
|
|
78
118
|
const runArgs = list.filter((a) => a !== '--yes' && a !== '-y' && a !== '--json');
|
|
79
119
|
return new Promise((resolve, reject) => {
|
|
80
120
|
if (!fs.existsSync(SCRIPT)) {
|
|
@@ -99,4 +139,4 @@ function spaceship(args = []) {
|
|
|
99
139
|
});
|
|
100
140
|
}
|
|
101
141
|
|
|
102
|
-
module.exports = { spaceship, SCRIPT, printUsage, planLines };
|
|
142
|
+
module.exports = { spaceship, SCRIPT, printUsage, planLines, spokenHours, spaceshipTargetRoot };
|
package/commands/status.js
CHANGED
|
@@ -4,6 +4,15 @@ const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/journa
|
|
|
4
4
|
const { parseTodo, getTeamActivity } = require('../lib/todo');
|
|
5
5
|
const { clarify } = require('../lib/autoland');
|
|
6
6
|
const { checkoutBehindMessage } = require('../lib/checkout-sync');
|
|
7
|
+
const {
|
|
8
|
+
isFreshWorkspace,
|
|
9
|
+
isKeepWorkingMinute,
|
|
10
|
+
isClaimMinute,
|
|
11
|
+
buildFirstMinute,
|
|
12
|
+
speakFirstMinute,
|
|
13
|
+
speakKeepWorkingMinute,
|
|
14
|
+
speakNothingRunning,
|
|
15
|
+
} = require('../lib/first-minute');
|
|
7
16
|
|
|
8
17
|
// Box drawing helpers
|
|
9
18
|
const W = 64; // inner width
|
|
@@ -122,18 +131,40 @@ function parseStatusTodo(todoFile) {
|
|
|
122
131
|
}
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
function hasLiveKeepWorkingRun(root = process.cwd()) {
|
|
135
|
+
try {
|
|
136
|
+
const { pickLiveLocalMission } = require('./mission');
|
|
137
|
+
return Boolean(pickLiveLocalMission(root));
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
125
143
|
function statusAtris(isQuick = false, jsonMode = false, verbose = false) {
|
|
126
|
-
|
|
144
|
+
// Fresh folder: empty talks first-talk. A file already here
|
|
145
|
+
// names that file, same as bare atris. Do not mint a room.
|
|
146
|
+
if (isFreshWorkspace()) {
|
|
147
|
+
process.exit(speakNothingRunning({ asJson: jsonMode }));
|
|
148
|
+
}
|
|
127
149
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
// Just-minted file folder, nothing running: same two lines as
|
|
151
|
+
// first-minute / the next keep-working ready. After init, next is claim: same
|
|
152
|
+
// two lines as bare atris / now. Not factory let-it-run.
|
|
153
|
+
// A live mission still gets the board. --verbose keeps the
|
|
154
|
+
// factory dump because the operator asked for it. --json on
|
|
155
|
+
// the claim path keeps the factory board for scripts.
|
|
156
|
+
if (!verbose && !hasLiveKeepWorkingRun()) {
|
|
157
|
+
const minute = buildFirstMinute({ root: process.cwd() });
|
|
158
|
+
if (isKeepWorkingMinute(minute)) {
|
|
159
|
+
process.exit(speakKeepWorkingMinute({ asJson: jsonMode }));
|
|
160
|
+
}
|
|
161
|
+
if (!jsonMode && isClaimMinute(minute)) {
|
|
162
|
+
process.exit(speakFirstMinute());
|
|
132
163
|
}
|
|
133
|
-
console.log('✗ atris/ folder not found. Run "atris init" first.');
|
|
134
|
-
process.exit(1);
|
|
135
164
|
}
|
|
136
165
|
|
|
166
|
+
const targetDir = path.join(process.cwd(), 'atris');
|
|
167
|
+
|
|
137
168
|
// Load task board state.
|
|
138
169
|
const todoFile = path.join(targetDir, 'TODO.md');
|
|
139
170
|
const todo = parseStatusTodo(todoFile);
|
package/commands/task.js
CHANGED
|
@@ -60,7 +60,18 @@ const {
|
|
|
60
60
|
decisionMarkerFor,
|
|
61
61
|
DECISION_REFUSE_REASON,
|
|
62
62
|
} = require('../lib/task-decision');
|
|
63
|
-
const {
|
|
63
|
+
const {
|
|
64
|
+
buildFirstMinute,
|
|
65
|
+
deskNextCommand,
|
|
66
|
+
firstTalkCommand,
|
|
67
|
+
folderName,
|
|
68
|
+
personName,
|
|
69
|
+
pickNext,
|
|
70
|
+
speakFirstMinute,
|
|
71
|
+
taskCommand,
|
|
72
|
+
taskNextCommand,
|
|
73
|
+
} = require('../lib/first-minute');
|
|
74
|
+
const { loadContext } = require('../lib/state-detection');
|
|
64
75
|
|
|
65
76
|
const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
|
|
66
77
|
|| process.env.USER
|
|
@@ -6172,9 +6183,9 @@ function cmdAdd(args) {
|
|
|
6172
6183
|
if (isUninitializedTaskFolder(root)) {
|
|
6173
6184
|
if (wantsJson(args)) {
|
|
6174
6185
|
printJson({
|
|
6175
|
-
ok:
|
|
6176
|
-
action: '
|
|
6177
|
-
command:
|
|
6186
|
+
ok: false,
|
|
6187
|
+
action: 'none',
|
|
6188
|
+
command: firstTalkCommand(folderName(root)),
|
|
6178
6189
|
task_id: null,
|
|
6179
6190
|
projection_path: null,
|
|
6180
6191
|
task: null,
|
|
@@ -6493,9 +6504,19 @@ function cmdDay(args) {
|
|
|
6493
6504
|
|
|
6494
6505
|
function cmdFirstMinute() {
|
|
6495
6506
|
const root = process.cwd();
|
|
6507
|
+
const fresh = !fs.existsSync(path.join(root, 'atris'));
|
|
6508
|
+
let context = {};
|
|
6509
|
+
if (!fresh) {
|
|
6510
|
+
try {
|
|
6511
|
+
context = loadContext(root);
|
|
6512
|
+
} catch {
|
|
6513
|
+
context = {};
|
|
6514
|
+
}
|
|
6515
|
+
}
|
|
6496
6516
|
const screen = buildFirstMinute({
|
|
6497
6517
|
root,
|
|
6498
|
-
fresh
|
|
6518
|
+
fresh,
|
|
6519
|
+
context,
|
|
6499
6520
|
});
|
|
6500
6521
|
console.log(screen.text);
|
|
6501
6522
|
}
|
|
@@ -6826,10 +6847,12 @@ function createEndgameSeedTask(taskDb, db, seed, owner) {
|
|
|
6826
6847
|
}
|
|
6827
6848
|
|
|
6828
6849
|
function nextActionFromCommand(command) {
|
|
6829
|
-
const
|
|
6850
|
+
const text = String(command || '').trim();
|
|
6851
|
+
const match = text.match(/^atris (?:task|mission) (\S+)/);
|
|
6830
6852
|
const verb = match ? match[1] : '';
|
|
6831
|
-
if (
|
|
6832
|
-
return
|
|
6853
|
+
if (verb && verb !== 'new') return verb;
|
|
6854
|
+
if (/^atris do\b/.test(text)) return 'do';
|
|
6855
|
+
return 'none';
|
|
6833
6856
|
}
|
|
6834
6857
|
|
|
6835
6858
|
function cmdNextTruth(args) {
|
|
@@ -6840,9 +6863,24 @@ function cmdNextTruth(args) {
|
|
|
6840
6863
|
const workspaceRoot = taskDb.workspaceRoot();
|
|
6841
6864
|
const rows = taskDb.listTasks(db, { workspaceRoot, limit: 500 });
|
|
6842
6865
|
const existingProj = readProjectionFile(workspaceRoot);
|
|
6866
|
+
const dbHasActionable = rows.some((task) => {
|
|
6867
|
+
const status = task && task.status;
|
|
6868
|
+
return status === 'open' || status === 'claimed' || status === 'review';
|
|
6869
|
+
});
|
|
6870
|
+
const projHasActionable = Boolean(
|
|
6871
|
+
existingProj
|
|
6872
|
+
&& Array.isArray(existingProj.tasks)
|
|
6873
|
+
&& existingProj.tasks.some((task) => {
|
|
6874
|
+
const status = task && task.status;
|
|
6875
|
+
return status === 'open' || status === 'claimed' || status === 'review';
|
|
6876
|
+
}),
|
|
6877
|
+
);
|
|
6843
6878
|
let projection;
|
|
6844
6879
|
let outPath;
|
|
6845
|
-
if (
|
|
6880
|
+
if (!dbHasActionable && projHasActionable) {
|
|
6881
|
+
projection = existingProj;
|
|
6882
|
+
outPath = path.resolve(path.join(workspaceRoot || '.', '.atris', 'state', 'tasks.projection.json'));
|
|
6883
|
+
} else if (rows.length === 0 && existingProj && Array.isArray(existingProj.tasks) && existingProj.tasks.length > 0) {
|
|
6846
6884
|
projection = existingProj;
|
|
6847
6885
|
outPath = path.resolve(path.join(workspaceRoot || '.', '.atris', 'state', 'tasks.projection.json'));
|
|
6848
6886
|
} else {
|
|
@@ -6911,9 +6949,9 @@ function cmdNext(args) {
|
|
|
6911
6949
|
if (isUninitializedTaskFolder(root)) {
|
|
6912
6950
|
if (wantsJson(args)) {
|
|
6913
6951
|
printJson({
|
|
6914
|
-
ok:
|
|
6915
|
-
action: '
|
|
6916
|
-
command:
|
|
6952
|
+
ok: false,
|
|
6953
|
+
action: 'none',
|
|
6954
|
+
command: firstTalkCommand(folderName(root)),
|
|
6917
6955
|
task_id: null,
|
|
6918
6956
|
owner: String(flag(args, '--as') || personName() || DEFAULT_OWNER),
|
|
6919
6957
|
scope: normalizeTaskQueueScope(taskQueueScopeFromArgs(args)),
|
|
@@ -6922,9 +6960,7 @@ function cmdNext(args) {
|
|
|
6922
6960
|
});
|
|
6923
6961
|
return;
|
|
6924
6962
|
}
|
|
6925
|
-
|
|
6926
|
-
console.log(screen.text);
|
|
6927
|
-
return;
|
|
6963
|
+
return speakFirstMinute({ root, fresh: true });
|
|
6928
6964
|
}
|
|
6929
6965
|
if (!hasFlag(args, '--create-next')) return cmdNextTruth(args);
|
|
6930
6966
|
const owner = flag(args, '--as') || DEFAULT_OWNER;
|
|
@@ -9796,16 +9832,17 @@ function appendRelabelArchivedJournalReceipt(workspaceRoot, { actor, count, ids
|
|
|
9796
9832
|
function cmdReady(args) {
|
|
9797
9833
|
const pos = positional(args);
|
|
9798
9834
|
const id = pos[0];
|
|
9799
|
-
|
|
9800
|
-
|
|
9835
|
+
const proofFlag = flag(args, '--proof');
|
|
9836
|
+
const verifyFlag = flag(args, '--verify');
|
|
9837
|
+
const resultFlag = textFlag(args, ['--result']);
|
|
9838
|
+
if (!id || (!proofFlag && !verifyFlag && !resultFlag)) {
|
|
9839
|
+
console.log('Usage: atris task ready <id> --proof "..." --result "<sentence>"');
|
|
9801
9840
|
process.exit(2);
|
|
9802
9841
|
}
|
|
9803
9842
|
// Two ways to prove: --proof "<note>" (claimed, pattern-checked) or
|
|
9804
9843
|
// --verify "<command>" which actually RUNS the command and gates ready on exit 0.
|
|
9805
9844
|
// If --proof names npm test, node --test, or git diff --check, this process
|
|
9806
9845
|
// must have run that command. A sentence that names one of those is a lie.
|
|
9807
|
-
const proofFlag = flag(args, '--proof');
|
|
9808
|
-
const verifyFlag = flag(args, '--verify');
|
|
9809
9846
|
const proofUrl = textFlag(args, ['--proof-url']);
|
|
9810
9847
|
const iFetched = hasFlag(args, '--i-fetched');
|
|
9811
9848
|
if (proofUrl && !iFetched) {
|
package/commands/terminal.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* atris terminal <business> <command...> [--timeout N]
|
|
3
3
|
*
|
|
4
4
|
* Run a shell command directly on a business EC2 workspace via the warm runner.
|
|
5
|
-
* This is the load-bearing primitive for fast bulk ops
|
|
5
|
+
* This is the load-bearing primitive for fast bulk ops: one bash call beats
|
|
6
6
|
* hundreds of rate-limited individual file API calls.
|
|
7
7
|
*
|
|
8
8
|
* SAFETY:
|
|
@@ -27,7 +27,7 @@ const path = require('path');
|
|
|
27
27
|
const { loadCredentials, abortOnAuthFailure } = require('../utils/auth');
|
|
28
28
|
const { apiRequestJson } = require('../utils/api');
|
|
29
29
|
const { loadBusinesses, saveBusinesses } = require('./business');
|
|
30
|
-
const { requireAccountBound, refuseAccountGlobal } = require('../lib/account-bound');
|
|
30
|
+
const { looksLikeBusinessSlug, requireAccountBound, refuseAccountGlobal } = require('../lib/account-bound');
|
|
31
31
|
const { argsWantHelp, wantsJson } = require('../lib/noninteractive');
|
|
32
32
|
|
|
33
33
|
function sleep(ms) {
|
|
@@ -151,8 +151,8 @@ async function terminalAtris() {
|
|
|
151
151
|
try { return JSON.parse(fs.readFileSync(bizFile, 'utf8')).slug; } catch { return null; }
|
|
152
152
|
})();
|
|
153
153
|
|
|
154
|
-
//
|
|
155
|
-
const firstLooksLikeSlug =
|
|
154
|
+
// Flags like --json match a hyphenated word regex. Require a real slug first.
|
|
155
|
+
const firstLooksLikeSlug = looksLikeBusinessSlug(args[0]);
|
|
156
156
|
|
|
157
157
|
if (firstLooksLikeSlug && args.length > 1) {
|
|
158
158
|
slug = args[0];
|
|
@@ -161,7 +161,7 @@ async function terminalAtris() {
|
|
|
161
161
|
slug = cwdSlug;
|
|
162
162
|
command = args.join(' ');
|
|
163
163
|
} else if (firstLooksLikeSlug && args.length === 1) {
|
|
164
|
-
// First
|
|
164
|
+
// First and only arg is a slug-shaped word with no command.
|
|
165
165
|
console.error('Missing command. Usage: atris terminal <business> <command>');
|
|
166
166
|
process.exit(1);
|
|
167
167
|
} else {
|
package/commands/workflow.js
CHANGED
|
@@ -6,13 +6,16 @@ const {
|
|
|
6
6
|
folderName,
|
|
7
7
|
freshMinuteJson,
|
|
8
8
|
isCertifiedReview,
|
|
9
|
+
listUserVisibleWork,
|
|
9
10
|
isFreshWorkspace,
|
|
10
11
|
personName,
|
|
11
12
|
pickNext,
|
|
12
13
|
renderWorkspace,
|
|
13
14
|
speakFirstMinute,
|
|
14
15
|
taskCommand,
|
|
16
|
+
visibleWorkTitle,
|
|
15
17
|
} = require('../lib/first-minute');
|
|
18
|
+
const { startFirstTalk } = require('../lib/context-gatherer');
|
|
16
19
|
const { isNonInteractive } = require('../lib/noninteractive');
|
|
17
20
|
const { loadContext } = require('../lib/state-detection');
|
|
18
21
|
const { buildToolResultBody } = require('../lib/tool-result-encode');
|
|
@@ -64,7 +67,8 @@ function reviewSoftTitle(title, maxWords = 5) {
|
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
function isHumanDeskNext(command) {
|
|
67
|
-
|
|
70
|
+
const text = String(command || '');
|
|
71
|
+
return /^atris do\b/.test(text) || /^atris task (?:claim|show|ready|accept)\b/.test(text);
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
function loadReviewTasks(root = process.cwd()) {
|
|
@@ -506,7 +510,7 @@ async function planAtris(userInput = null) {
|
|
|
506
510
|
// init --minimal is optional context, not a factory bounce.
|
|
507
511
|
if (!fs.existsSync(targetDir)) {
|
|
508
512
|
if (args.includes('--json')) {
|
|
509
|
-
console.log(JSON.stringify(freshMinuteJson(), null, 2));
|
|
513
|
+
console.log(JSON.stringify(freshMinuteJson(folderName(cwd), listUserVisibleWork(cwd), { root: cwd }), null, 2));
|
|
510
514
|
process.exit(2);
|
|
511
515
|
}
|
|
512
516
|
const screen = buildFirstMinute({ root: cwd, fresh: true });
|
|
@@ -877,11 +881,21 @@ async function doAtris() {
|
|
|
877
881
|
const cwd = process.cwd();
|
|
878
882
|
const targetDir = path.join(cwd, 'atris');
|
|
879
883
|
|
|
880
|
-
// Empty folder talks like bare atris.
|
|
881
|
-
//
|
|
884
|
+
// Empty folder talks like bare atris. Files already here start
|
|
885
|
+
// first-talk, then next is atris do. After that work is yours,
|
|
886
|
+
// next is task ready so keep-working is not a do loop. Missing
|
|
887
|
+
// executor.md after init --minimal is optional context, not a
|
|
888
|
+
// factory bounce.
|
|
882
889
|
if (!fs.existsSync(targetDir)) {
|
|
890
|
+
const visible = listUserVisibleWork(cwd);
|
|
891
|
+
if (visible.length) {
|
|
892
|
+
const title = visibleWorkTitle(visible, folderName(cwd));
|
|
893
|
+
const code = startFirstTalk(cwd, title, { asJson: args.includes('--json') });
|
|
894
|
+
if (code !== 0) process.exit(code);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
883
897
|
if (args.includes('--json')) {
|
|
884
|
-
console.log(JSON.stringify(freshMinuteJson(), null, 2));
|
|
898
|
+
console.log(JSON.stringify(freshMinuteJson(folderName(cwd), visible, { root: cwd }), null, 2));
|
|
885
899
|
process.exit(2);
|
|
886
900
|
}
|
|
887
901
|
const screen = buildFirstMinute({ root: cwd, fresh: true });
|
package/commands/x-search.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const { apiRequestJson } = require('../utils/api');
|
|
4
4
|
const { ensureBilledCommandAuth } = require('./auth');
|
|
5
|
+
const applyGate = require('../lib/apply-gate');
|
|
5
6
|
|
|
6
7
|
const DEFAULT_TIMEOUT_MS = 120000;
|
|
7
8
|
const COST_HINT = '5 credits per search';
|
|
9
|
+
const APPLY_NEXT_MESSAGE =
|
|
10
|
+
'next: write one apply (change + receipt) for this query.';
|
|
8
11
|
|
|
9
12
|
function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
|
|
10
13
|
output('');
|
|
@@ -13,6 +16,7 @@ function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
|
|
|
13
16
|
output('');
|
|
14
17
|
output(`Search X/Twitter via Atris (${COST_HINT}).`);
|
|
15
18
|
output('Requires login. Same auth path as atris youtube process.');
|
|
19
|
+
output('Empty or failed search refunds the credits.');
|
|
16
20
|
output('');
|
|
17
21
|
output('Options:');
|
|
18
22
|
output(' --limit <n> Max results hint (search only)');
|
|
@@ -213,6 +217,58 @@ function resultErrorText(result) {
|
|
|
213
217
|
}
|
|
214
218
|
}
|
|
215
219
|
|
|
220
|
+
function unwrapXSearchPayload(data) {
|
|
221
|
+
return data?.data && typeof data.data === 'object' ? data.data : data;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function xSearchContent(data) {
|
|
225
|
+
const payload = unwrapXSearchPayload(data);
|
|
226
|
+
return payload?.content != null ? String(payload.content).trim() : '';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function xSearchCitations(data) {
|
|
230
|
+
const payload = unwrapXSearchPayload(data);
|
|
231
|
+
if (Array.isArray(payload?.citations)) return payload.citations;
|
|
232
|
+
if (Array.isArray(data?.citations)) return data.citations;
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function xSearchCredits(data) {
|
|
237
|
+
if (!data || typeof data !== 'object') {
|
|
238
|
+
return { used: undefined, remaining: undefined, refunded: undefined };
|
|
239
|
+
}
|
|
240
|
+
const payload = unwrapXSearchPayload(data) || {};
|
|
241
|
+
const used = data.credits_used !== undefined ? data.credits_used : payload.credits_used;
|
|
242
|
+
const remaining = data.credits_remaining !== undefined
|
|
243
|
+
? data.credits_remaining
|
|
244
|
+
: payload.credits_remaining;
|
|
245
|
+
let refunded = data.credits_refunded !== undefined
|
|
246
|
+
? data.credits_refunded
|
|
247
|
+
: payload.credits_refunded;
|
|
248
|
+
if (refunded === undefined && (data.refunded === true || payload.refunded === true)) {
|
|
249
|
+
refunded = true;
|
|
250
|
+
}
|
|
251
|
+
return { used, remaining, refunded };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function creditsWereRefunded(credits) {
|
|
255
|
+
if (!credits) return false;
|
|
256
|
+
if (credits.used === 0) return true;
|
|
257
|
+
if (credits.refunded === true) return true;
|
|
258
|
+
return typeof credits.refunded === 'number' && credits.refunded > 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function formatCreditsLines(credits) {
|
|
262
|
+
const lines = [];
|
|
263
|
+
if (credits.used !== undefined || credits.remaining !== undefined) {
|
|
264
|
+
lines.push(`Credits: ${credits.used !== undefined ? credits.used : '?'} used, ${credits.remaining !== undefined ? credits.remaining : '?'} remaining`);
|
|
265
|
+
}
|
|
266
|
+
if (creditsWereRefunded(credits)) {
|
|
267
|
+
lines.push('credits refunded');
|
|
268
|
+
}
|
|
269
|
+
return lines;
|
|
270
|
+
}
|
|
271
|
+
|
|
216
272
|
function xSearchFailureError(result) {
|
|
217
273
|
const hint = result.status === 401
|
|
218
274
|
? ' Run "atris login --force".'
|
|
@@ -221,7 +277,13 @@ function xSearchFailureError(result) {
|
|
|
221
277
|
: result.status === 502
|
|
222
278
|
? ' xAI is unavailable; retry in a few seconds.'
|
|
223
279
|
: '';
|
|
224
|
-
|
|
280
|
+
const credits = xSearchCredits(result.data);
|
|
281
|
+
const refundHint = result.status === 502 && creditsWereRefunded(credits)
|
|
282
|
+
? ' credits refunded.'
|
|
283
|
+
: '';
|
|
284
|
+
const lines = [`X search failed (${result.status}): ${resultErrorText(result)}.${hint}${refundHint}`];
|
|
285
|
+
lines.push(...formatCreditsLines(credits));
|
|
286
|
+
return new Error(lines.join('\n'));
|
|
225
287
|
}
|
|
226
288
|
|
|
227
289
|
async function ensureToken(deps = {}) {
|
|
@@ -265,13 +327,35 @@ async function runXSearch(options, deps = {}) {
|
|
|
265
327
|
return result.data;
|
|
266
328
|
}
|
|
267
329
|
|
|
330
|
+
function xSearchApplySource(options) {
|
|
331
|
+
if (options?.mode === 'person') return options.name;
|
|
332
|
+
return options?.query;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function xSearchApplyRel(source) {
|
|
336
|
+
return applyGate.applySidecarRel('x-search', applyGate.applySlug(source));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function xSearchHasResults(data) {
|
|
340
|
+
return Boolean(xSearchContent(data)) || xSearchCitations(data).length > 0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function ensureXSearchApply({ cwd, source, now, output } = {}) {
|
|
344
|
+
return applyGate.ensureApply({
|
|
345
|
+
cwd,
|
|
346
|
+
source,
|
|
347
|
+
rel: source ? xSearchApplyRel(source) : null,
|
|
348
|
+
now,
|
|
349
|
+
output,
|
|
350
|
+
incompleteMessage: APPLY_NEXT_MESSAGE,
|
|
351
|
+
required: false,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
268
355
|
function formatXSearchResult(data) {
|
|
269
356
|
const lines = [];
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
const citations = Array.isArray(payload?.citations)
|
|
273
|
-
? payload.citations
|
|
274
|
-
: (Array.isArray(data?.citations) ? data.citations : []);
|
|
357
|
+
const content = xSearchContent(data);
|
|
358
|
+
const citations = xSearchCitations(data);
|
|
275
359
|
|
|
276
360
|
if (content) {
|
|
277
361
|
lines.push(content);
|
|
@@ -289,18 +373,25 @@ function formatXSearchResult(data) {
|
|
|
289
373
|
}
|
|
290
374
|
}
|
|
291
375
|
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
? data.credits_remaining
|
|
295
|
-
: payload?.credits_remaining;
|
|
296
|
-
if (used !== undefined || remaining !== undefined) {
|
|
376
|
+
const creditLines = formatCreditsLines(xSearchCredits(data));
|
|
377
|
+
if (creditLines.length) {
|
|
297
378
|
lines.push('');
|
|
298
|
-
lines.push(
|
|
379
|
+
lines.push(...creditLines);
|
|
299
380
|
}
|
|
300
381
|
|
|
301
382
|
return lines.join('\n');
|
|
302
383
|
}
|
|
303
384
|
|
|
385
|
+
function formatEmptyXSearchResult(data) {
|
|
386
|
+
const lines = ['no results'];
|
|
387
|
+
const creditLines = formatCreditsLines(xSearchCredits(data));
|
|
388
|
+
if (creditLines.length) {
|
|
389
|
+
lines.push('');
|
|
390
|
+
lines.push(...creditLines);
|
|
391
|
+
}
|
|
392
|
+
return lines.join('\n');
|
|
393
|
+
}
|
|
394
|
+
|
|
304
395
|
async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
|
|
305
396
|
const output = deps.output || ((line = '') => console.log(line));
|
|
306
397
|
let options;
|
|
@@ -319,7 +410,23 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
|
|
|
319
410
|
let status = 0;
|
|
320
411
|
try {
|
|
321
412
|
const data = await runXSearch(options, deps);
|
|
322
|
-
|
|
413
|
+
const hasResults = xSearchHasResults(data);
|
|
414
|
+
if (options.json) {
|
|
415
|
+
output(JSON.stringify(data, null, 2));
|
|
416
|
+
} else {
|
|
417
|
+
output(hasResults ? formatXSearchResult(data) : formatEmptyXSearchResult(data));
|
|
418
|
+
}
|
|
419
|
+
if (hasResults) {
|
|
420
|
+
const ensureApply = deps.ensureApply || ensureXSearchApply;
|
|
421
|
+
status = ensureApply({
|
|
422
|
+
cwd: deps.cwd || process.cwd(),
|
|
423
|
+
source: xSearchApplySource(options),
|
|
424
|
+
now: deps.applyNow,
|
|
425
|
+
output,
|
|
426
|
+
});
|
|
427
|
+
} else {
|
|
428
|
+
status = 2;
|
|
429
|
+
}
|
|
323
430
|
} catch (err) {
|
|
324
431
|
output(err.message);
|
|
325
432
|
status = 1;
|
|
@@ -332,9 +439,12 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
|
|
|
332
439
|
|
|
333
440
|
module.exports = {
|
|
334
441
|
DEFAULT_TIMEOUT_MS,
|
|
442
|
+
APPLY_NEXT_MESSAGE,
|
|
335
443
|
parseXSearchArgs,
|
|
336
444
|
buildSearchPayload,
|
|
337
445
|
buildPersonPayload,
|
|
338
446
|
formatXSearchResult,
|
|
447
|
+
xSearchHasResults,
|
|
448
|
+
xSearchApplyRel,
|
|
339
449
|
xSearchCommand,
|
|
340
450
|
};
|