atris 3.56.0 → 3.56.1
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 +119 -87
- package/commands/autopilot-front.js +29 -13
- package/commands/brainstorm.js +77 -476
- package/commands/business.js +13 -1
- package/commands/fleet-report.js +2 -2
- package/commands/human-missions.js +26 -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/log.js +79 -30
- package/commands/mission.js +117 -22
- package/commands/next.js +153 -63
- package/commands/now.js +80 -38
- package/commands/recap.js +66 -4
- package/commands/run-front.js +20 -5
- package/commands/spaceship.js +52 -12
- package/commands/status.js +30 -7
- package/commands/task.js +56 -19
- package/commands/terminal.js +5 -5
- package/commands/workflow.js +18 -5
- package/commands/x-search.js +123 -13
- package/commands/youtube.js +639 -36
- package/lib/account-bound.js +7 -0
- package/lib/apply-gate.js +94 -0
- package/lib/context-gatherer.js +55 -8
- package/lib/engine-ask.js +16 -6
- package/lib/first-minute.js +552 -26
- package/lib/known-commands.js +1 -1
- package/lib/runner-command.js +5 -3
- package/lib/scratch-root.js +44 -0
- package/package.json +1 -1
- package/utils/auth.js +9 -0
package/lib/account-bound.js
CHANGED
|
@@ -15,6 +15,12 @@ function isHelpArg(arg) {
|
|
|
15
15
|
return arg === '--help' || arg === '-h' || arg === 'help';
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function looksLikeBusinessSlug(token) {
|
|
19
|
+
const value = String(token || '');
|
|
20
|
+
if (!value || value.startsWith('-')) return false;
|
|
21
|
+
return /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
/**
|
|
19
25
|
* Account-scoped verbs dump live CRM/cloud state. Outside a bound business
|
|
20
26
|
* workspace they must opt in with --account. Help always passes.
|
|
@@ -42,6 +48,7 @@ function refuseAccountGlobal(write = console.error) {
|
|
|
42
48
|
|
|
43
49
|
module.exports = {
|
|
44
50
|
ACCOUNT_GLOBAL_MESSAGE,
|
|
51
|
+
looksLikeBusinessSlug,
|
|
45
52
|
requireAccountBound,
|
|
46
53
|
refuseAccountGlobal,
|
|
47
54
|
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function dateStamp(now) {
|
|
7
|
+
if (typeof now === 'string' && /^\d{4}-\d{2}-\d{2}/.test(now)) {
|
|
8
|
+
return now.slice(0, 10);
|
|
9
|
+
}
|
|
10
|
+
const value = now instanceof Date ? now : new Date(now || Date.now());
|
|
11
|
+
if (Number.isNaN(value.getTime())) return new Date().toISOString().slice(0, 10);
|
|
12
|
+
return value.toISOString().slice(0, 10);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseApplyFields(text) {
|
|
16
|
+
const change = String(text || '').match(/^change:\s*(.+)$/im);
|
|
17
|
+
const receipt = String(text || '').match(/^receipt:\s*(.+)$/im);
|
|
18
|
+
return {
|
|
19
|
+
change: change ? change[1].trim() : '',
|
|
20
|
+
receipt: receipt ? receipt[1].trim() : '',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isFilledApply(fields) {
|
|
25
|
+
const empty = (value) => !value || /^fill this$/i.test(value);
|
|
26
|
+
return !empty(fields.change) && !empty(fields.receipt);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function applySlug(text) {
|
|
30
|
+
const slug = String(text || '')
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
33
|
+
.replace(/^-+|-+$/g, '')
|
|
34
|
+
.slice(0, 48);
|
|
35
|
+
return slug || 'query';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function applySidecarRel(kind, id) {
|
|
39
|
+
return `atris/wiki/briefs/${kind}-${id}.apply.md`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readApplyReceipt({ cwd, rel } = {}) {
|
|
43
|
+
if (!rel || !cwd) return null;
|
|
44
|
+
const abs = path.join(cwd, rel);
|
|
45
|
+
if (!fs.existsSync(abs)) return null;
|
|
46
|
+
return { rel, text: fs.readFileSync(abs, 'utf8') };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function writeApplyStub({ cwd, source, rel, now } = {}) {
|
|
50
|
+
try {
|
|
51
|
+
if (!rel || !cwd) return null;
|
|
52
|
+
const wikiDir = path.join(cwd, 'atris', 'wiki');
|
|
53
|
+
if (!fs.existsSync(wikiDir)) return null;
|
|
54
|
+
|
|
55
|
+
const abs = path.join(cwd, rel);
|
|
56
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
57
|
+
if (!fs.existsSync(abs)) {
|
|
58
|
+
fs.writeFileSync(abs, [
|
|
59
|
+
`source: ${source}`,
|
|
60
|
+
'change: fill this',
|
|
61
|
+
'receipt: fill this',
|
|
62
|
+
].join('\n') + '\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const date = dateStamp(now);
|
|
66
|
+
const journalPath = path.join(cwd, 'atris', 'logs', date.slice(0, 4), `${date}.md`);
|
|
67
|
+
fs.mkdirSync(path.dirname(journalPath), { recursive: true });
|
|
68
|
+
let existing = '';
|
|
69
|
+
if (fs.existsSync(journalPath)) existing = fs.readFileSync(journalPath, 'utf8');
|
|
70
|
+
const line = `- [claimable] apply: fill this -> ${rel}`;
|
|
71
|
+
if (!existing.includes(line)) {
|
|
72
|
+
const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
73
|
+
fs.writeFileSync(journalPath, `${existing}${prefix}${line}\n`);
|
|
74
|
+
}
|
|
75
|
+
return rel;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ensureApply({ cwd, source, rel, now, output, incompleteMessage, required = true } = {}) {
|
|
82
|
+
const print = typeof output === 'function' ? output : (line = '') => console.error(line);
|
|
83
|
+
const existing = readApplyReceipt({ cwd, rel });
|
|
84
|
+
if (existing && isFilledApply(parseApplyFields(existing.text))) return 0;
|
|
85
|
+
if (!(required && existing)) writeApplyStub({ cwd, source, rel, now });
|
|
86
|
+
print(incompleteMessage);
|
|
87
|
+
return required ? 2 : 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
applySlug,
|
|
92
|
+
applySidecarRel,
|
|
93
|
+
ensureApply,
|
|
94
|
+
};
|
package/lib/context-gatherer.js
CHANGED
|
@@ -54,9 +54,11 @@ function isFlagLikeAnswer(answer) {
|
|
|
54
54
|
return tokens.every((token) => token.startsWith('-') || token === 'help');
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function starterTaskTitle(answer) {
|
|
58
|
-
const
|
|
59
|
-
|
|
57
|
+
function starterTaskTitle(answer, folder = 'this folder') {
|
|
58
|
+
const room = folder || 'this folder';
|
|
59
|
+
const { isFirstTalkLine } = require('./first-minute');
|
|
60
|
+
if (isFirstTalkLine(answer)) return room;
|
|
61
|
+
return compactText(answer, 80) || room;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
function saveContextProfile(root, answer, { source = 'first_contact' } = {}) {
|
|
@@ -77,29 +79,71 @@ function saveContextProfile(root, answer, { source = 'first_contact' } = {}) {
|
|
|
77
79
|
return profile;
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
function runSilentMinimalInit(root = process.cwd()) {
|
|
83
|
+
const { spawnSync } = require('child_process');
|
|
84
|
+
const script = process.argv[1] || path.join(__dirname, '..', 'bin', 'atris.js');
|
|
85
|
+
const result = spawnSync(process.execPath, [script, 'init', '--minimal', '--yes'], {
|
|
86
|
+
cwd: root,
|
|
87
|
+
env: process.env,
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
90
|
+
});
|
|
91
|
+
return Number.isInteger(result.status) ? result.status : 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function startFirstTalk(root, answer, { asJson = false, log = console.log } = {}) {
|
|
95
|
+
const {
|
|
96
|
+
firstTalkJson,
|
|
97
|
+
folderName,
|
|
98
|
+
personName,
|
|
99
|
+
renderFirstTalk,
|
|
100
|
+
} = require('./first-minute');
|
|
101
|
+
const initStatus = runSilentMinimalInit(root);
|
|
102
|
+
if (initStatus !== 0) return initStatus;
|
|
103
|
+
saveContextProfile(root, answer, { source: 'first_talk' });
|
|
104
|
+
const starter = createStarterTask(root, answer);
|
|
105
|
+
const room = folderName(root);
|
|
106
|
+
const who = personName();
|
|
107
|
+
if (asJson) {
|
|
108
|
+
log(JSON.stringify(firstTalkJson({ starter, person: who, folder: room }), null, 2));
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
log('');
|
|
112
|
+
log(renderFirstTalk({ person: who, folder: room, starter }));
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
80
116
|
function createStarterTask(root, answer) {
|
|
81
117
|
if (isFlagLikeAnswer(answer)) return null;
|
|
82
118
|
const atrisDir = path.join(root, 'atris');
|
|
83
119
|
if (!fs.existsSync(atrisDir)) return null;
|
|
120
|
+
const { folderName, personName } = require('./first-minute');
|
|
121
|
+
const title = starterTaskTitle(answer, folderName(root));
|
|
122
|
+
const who = personName() || 'operator';
|
|
84
123
|
try {
|
|
85
124
|
const taskDb = require('./task-db');
|
|
86
125
|
const db = taskDb.open();
|
|
87
126
|
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
88
|
-
const title = starterTaskTitle(answer);
|
|
89
127
|
const sourceKey = taskDb.sourceKey('context-gatherer:first-task', title);
|
|
90
128
|
const added = taskDb.addTask(db, {
|
|
91
129
|
title,
|
|
92
130
|
tag: 'onboarding',
|
|
93
131
|
workspaceRoot,
|
|
94
132
|
sourceKey,
|
|
133
|
+
status: 'claimed',
|
|
134
|
+
claimedBy: who,
|
|
95
135
|
metadata: {
|
|
96
136
|
source: 'context_gatherer',
|
|
97
137
|
first_answer: compactText(answer, 500),
|
|
98
138
|
},
|
|
99
139
|
});
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
140
|
+
let rows = taskDb.listTasks(db, { workspaceRoot });
|
|
141
|
+
let task = taskDb.withTaskDisplayRefs(rows).find(row => row.id === added.id) || null;
|
|
142
|
+
if (task && task.status === 'open') {
|
|
143
|
+
taskDb.claimTask(db, { id: added.id, claimedBy: who });
|
|
144
|
+
rows = taskDb.listTasks(db, { workspaceRoot });
|
|
145
|
+
task = taskDb.withTaskDisplayRefs(rows).find(row => row.id === added.id) || task;
|
|
146
|
+
}
|
|
103
147
|
try {
|
|
104
148
|
const todoPath = path.join(root, 'atris', 'TODO.md');
|
|
105
149
|
fs.writeFileSync(todoPath, taskDb.renderTodoMarkdown(rows, { title: 'TODO.md' }), 'utf8');
|
|
@@ -109,11 +153,13 @@ function createStarterTask(root, answer) {
|
|
|
109
153
|
inserted: added.inserted,
|
|
110
154
|
display_id: task && task.display_id || null,
|
|
111
155
|
title,
|
|
156
|
+
status: task && task.status || 'claimed',
|
|
157
|
+
claimed_by: (task && task.claimed_by) || who,
|
|
112
158
|
};
|
|
113
159
|
} catch (error) {
|
|
114
160
|
return {
|
|
115
161
|
error: error && error.message ? error.message : String(error),
|
|
116
|
-
title
|
|
162
|
+
title,
|
|
117
163
|
};
|
|
118
164
|
}
|
|
119
165
|
}
|
|
@@ -185,6 +231,7 @@ module.exports = {
|
|
|
185
231
|
hasContextProfile,
|
|
186
232
|
saveContextProfile,
|
|
187
233
|
createStarterTask,
|
|
234
|
+
startFirstTalk,
|
|
188
235
|
shouldGatherContext,
|
|
189
236
|
renderPrompt,
|
|
190
237
|
starterTaskTitle,
|
package/lib/engine-ask.js
CHANGED
|
@@ -25,6 +25,7 @@ const DEFAULT_ASK_TIMEOUT_MS = 120000;
|
|
|
25
25
|
const MAX_ASK_CONCURRENCY = 4;
|
|
26
26
|
const MAX_ASK_JOBS = 8;
|
|
27
27
|
const MAX_ASK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
28
|
+
const DEFAULT_FABLE_ASK_TIMEOUT_MS = MAX_ASK_TIMEOUT_MS;
|
|
28
29
|
const MAX_ASK_PROMPT_BYTES = 16 * 1024;
|
|
29
30
|
const MAX_ASK_TOTAL_PROMPT_BYTES = 64 * 1024;
|
|
30
31
|
const MAX_ASK_OUTPUT_BYTES = 1024 * 1024;
|
|
@@ -45,7 +46,7 @@ function askUsage() {
|
|
|
45
46
|
'options:',
|
|
46
47
|
' --model <name> exact model for every selected engine',
|
|
47
48
|
` --concurrency <n> parallel runs, 1-${MAX_ASK_CONCURRENCY} (default ${DEFAULT_ASK_CONCURRENCY})`,
|
|
48
|
-
` --timeout <sec> per-engine timeout, 1-${MAX_ASK_TIMEOUT_MS / 1000} (default ${DEFAULT_ASK_TIMEOUT_MS / 1000})`,
|
|
49
|
+
` --timeout <sec> per-engine timeout, 1-${MAX_ASK_TIMEOUT_MS / 1000} (default ${DEFAULT_ASK_TIMEOUT_MS / 1000}; Fable ${DEFAULT_FABLE_ASK_TIMEOUT_MS / 1000})`,
|
|
49
50
|
' --json print the receipt as json',
|
|
50
51
|
'',
|
|
51
52
|
`jobs files contain up to ${MAX_ASK_JOBS} objects: [{"engine":"codex","model":"optional","prompt":"question","label":"optional"}]`,
|
|
@@ -74,7 +75,7 @@ function normalizeAskJob(job, index) {
|
|
|
74
75
|
if (Buffer.byteLength(prompt) > MAX_ASK_PROMPT_BYTES) {
|
|
75
76
|
throw new Error(`job ${index + 1} prompt must be ${MAX_ASK_PROMPT_BYTES} bytes or fewer`);
|
|
76
77
|
}
|
|
77
|
-
const model =
|
|
78
|
+
const model = resolveAskModel(engine, job.model);
|
|
78
79
|
const label = String(job.label || '').trim();
|
|
79
80
|
if (label.length > 80) throw new Error(`job ${index + 1} label must be 80 characters or fewer`);
|
|
80
81
|
return { engine, model, prompt, label };
|
|
@@ -99,7 +100,7 @@ function parseEngineAskArgs(args, { root = process.cwd(), readFile = fs.readFile
|
|
|
99
100
|
let requestedModel = '';
|
|
100
101
|
let modelFlagPresent = false;
|
|
101
102
|
let concurrency = DEFAULT_ASK_CONCURRENCY;
|
|
102
|
-
let timeoutMs =
|
|
103
|
+
let timeoutMs = null;
|
|
103
104
|
let json = false;
|
|
104
105
|
let help = false;
|
|
105
106
|
|
|
@@ -146,7 +147,7 @@ function parseEngineAskArgs(args, { root = process.cwd(), readFile = fs.readFile
|
|
|
146
147
|
promptParts.push(arg);
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
if (help) return { help: true, json, jobs: [], concurrency, timeoutMs };
|
|
150
|
+
if (help) return { help: true, json, jobs: [], concurrency, timeoutMs: timeoutMs || DEFAULT_ASK_TIMEOUT_MS };
|
|
150
151
|
if (modelFlagPresent && !requestedModel) throw new Error('--model needs a name');
|
|
151
152
|
const commonPrompt = promptParts.join(' ').trim();
|
|
152
153
|
let jobs;
|
|
@@ -178,7 +179,9 @@ function parseEngineAskArgs(args, { root = process.cwd(), readFile = fs.readFile
|
|
|
178
179
|
if (totalPromptBytes > MAX_ASK_TOTAL_PROMPT_BYTES) {
|
|
179
180
|
throw new Error(`engine ask accepts at most ${MAX_ASK_TOTAL_PROMPT_BYTES} prompt bytes per run`);
|
|
180
181
|
}
|
|
181
|
-
|
|
182
|
+
const resolvedTimeoutMs = timeoutMs
|
|
183
|
+
|| (jobs.some((job) => job.engine === 'fable') ? DEFAULT_FABLE_ASK_TIMEOUT_MS : DEFAULT_ASK_TIMEOUT_MS);
|
|
184
|
+
return { help: false, json, jobs: labelAskJobs(jobs), concurrency, timeoutMs: resolvedTimeoutMs };
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
function guardedPrompt(prompt) {
|
|
@@ -192,11 +195,18 @@ function assertAskModelSupported(engine, model) {
|
|
|
192
195
|
throw error;
|
|
193
196
|
}
|
|
194
197
|
|
|
198
|
+
function resolveAskModel(engine, modelName = '') {
|
|
199
|
+
const requested = String(modelName || '').trim();
|
|
200
|
+
if (requested) return requested;
|
|
201
|
+
if (engine !== 'claude' && engine !== 'fable' && engine !== 'haiku') return '';
|
|
202
|
+
return RUNNER_PROFILE_DEFS[engine]?.model || DEFAULT_CLAUDE_RUNNER_MODEL;
|
|
203
|
+
}
|
|
204
|
+
|
|
195
205
|
function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '') {
|
|
196
206
|
const engine = canonicalEngineName(engineName);
|
|
197
207
|
const profile = RUNNER_PROFILE_DEFS[engine];
|
|
198
208
|
if (!profile) throw new Error(`unknown engine "${engineName}"`);
|
|
199
|
-
const model =
|
|
209
|
+
const model = resolveAskModel(engine, modelName);
|
|
200
210
|
assertAskModelSupported(engine, model);
|
|
201
211
|
const request = guardedPrompt(prompt);
|
|
202
212
|
|