Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@

## [Не выпущено]

## [0.2.0] - 2026-09-17

### Добавлено

- `answer --option "вариант"`: на вопрос с готовыми вариантами теперь можно
ответить. Раньше такой вопрос не отвечался вовсе — сервер требовал вариант из
списка, а передать его было нечем.
- `status` печатает варианты ответа строками под вопросом и говорит, когда
вопрос ждёт значения в `--comment`.

### Исправлено

- README: в примере с `jq` поле вопроса называется `question`, а не `text`.

## [0.1.1] - 2026-09-12

### Изменено
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ npx @autopasha/cli skill > .claude/skills/autopasha/SKILL.md
| `autopasha status` | чем занят сотрудник, что стоит, какие вопросы открыты |
| `autopasha feed [--since 2h]` | что произошло за срок |
| `autopasha ask "текст" [--why "..."]` | поставить задачу |
| `autopasha answer <id> да\|нет [--comment "..."]` | ответить на вопрос |
| `autopasha answer <id> да\|нет [--comment "..."] [--option "вариант"]` | ответить на вопрос |
| `autopasha say "текст"` | реплика в разговор, без задачи |
| `autopasha watch [--since 1h] [--interval 20]` | ждать событий и печатать построчно |
| `autopasha skill` | напечатать умение для своего агента |
Expand Down Expand Up @@ -125,10 +125,20 @@ autopasha answer 8f2ac1b0-2f5e-4a11-9a6b-6c0f5a1d2e34 да --comment "бери
Ответ будит сотрудника сразу, как кнопка в кабинете. Комментарий важнее самого
решения: по нему видно границы, а не только факт согласия.

Бывает, что сотрудник предложил к вопросу готовые варианты: в `status` они
видны строками под вопросом. Тогда вариант надо назвать, иначе ответ не примут:

```bash
autopasha answer 8f2ac1b0-2f5e-4a11-9a6b-6c0f5a1d2e34 да --option "на общий ящик"
```

А если вопрос ждёт значения (кода из СМС, суммы, ссылки), оно едет в
`--comment`: под вопросом так и написано, что именно заполнить.

Разобрать ответ кодом:

```bash
autopasha status --json | jq '.questions[] | {id, text}'
autopasha status --json | jq '.questions[] | {id, question, options}'
```

Два ключа на одной машине, если сотрудников у вас двое:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@autopasha/cli",
"version": "0.1.1",
"version": "0.2.0",
"description": "Командная строка АвтоПаши: чем занят AI-сотрудник, поставить ему задачу, ответить на его вопрос",
"keywords": [
"autopasha",
Expand Down
11 changes: 11 additions & 0 deletions skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ autopasha answer 8f2ac1b0-... да --comment "бери бесплатный ва
Комментарий важнее самого решения: по нему сотрудник поймёт границы, а не
только факт согласия. Ответ будит его сразу, как кнопка в кабинете.

Если под вопросом в `status` перечислены варианты строками «— …», ответ без
варианта не примут: назовите его надписью как есть.

```bash
autopasha answer 8f2ac1b0-... да --option "на общий ящик"
```

А когда под вопросом написано «нужно заполнить: …» - вопрос ждёт значения, и
оно едет в `--comment`. Это не разрешение, а сам код, сумма или ссылка, ради
которых вопрос и задан.

```bash
autopasha say "клиент передумал, каталог не трогай"
```
Expand Down
13 changes: 10 additions & 3 deletions src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const HELP = `autopasha — командная строка АвтоПаши.
autopasha status чем занят сотрудник, что стоит, что спрашивает
autopasha feed [--since 2h] что произошло
autopasha ask "текст" [--why "…"] поставить задачу
autopasha answer <id> да|нет [--comment "…"] ответить на вопрос
autopasha answer <id> да|нет [--comment "…"] [--option "вариант"] ответить на вопрос
autopasha say "текст" реплика в разговор
autopasha watch [--since 1h] ждать событий и печатать их построчно
autopasha skill напечатать умение для своего агента
Expand Down Expand Up @@ -184,12 +184,19 @@ async function cmdAnswer({ client, args, opts, out }) {
const [id, word] = args;
if (!id) throw new UsageError('нужен id вопроса: autopasha answer <id> да');
const decision = parseDecision(word);
const res = await client.answer(id, { decision, comment: opts.comment }, opts['idempotency-key']);
// `--option` — надпись варианта, если сотрудник их предложил. Смысл выбора
// («да» это или «нет») считает сервер по своей записи вопроса, мы передаём
// только надпись: вопрос про прод не должен переписываться из терминала.
const body = { decision };
if (opts.comment) body.comment = opts.comment;
if (opts.option) body.option = opts.option;
const res = await client.answer(id, body, opts['idempotency-key']);
if (opts.json) {
out(JSON.stringify(res, null, 2));
return 0;
}
out(`Ответ записан: ${decision === 'approved' ? 'да' : 'нет'}${opts.comment ? ` — ${opts.comment}` : ''}`);
const said = [opts.option, opts.comment].filter(Boolean).join(' — ');
out(`Ответ записан: ${decision === 'approved' ? 'да' : 'нет'}${said ? ` — ${said}` : ''}`);
out('Сотрудник разбужен и вернётся к этому делу.');
return 0;
}
Expand Down
11 changes: 11 additions & 0 deletions src/render.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,24 @@ export function renderStatus(s, now = new Date()) {
for (const q of s.questions) {
out.push(` ${q.id}`);
out.push(` ${q.question}`);
// Варианты и поле печатаем всегда, когда они есть: без них ответ на такой
// вопрос отбивается сервером, а догадаться, чего он хочет, не по чему.
for (const o of q.options ?? []) {
out.push(` — ${o.label}${o.hint ? ` (${o.hint})` : ''}`);
}
if (q.input) {
out.push(` нужно ${q.input.required ? 'заполнить' : 'значение'}: ${q.input.label} → --comment`);
}
const tail = [];
if (q.ifSilent) tail.push(`молчание значит «${q.ifSilent}»`);
else tail.push('без ответа встанет');
if (q.expiresAt) tail.push(`до ${clock(q.expiresAt, now)}`);
out.push(` ${tail.join(', ')}`);
}
out.push('', 'Ответить: autopasha answer <id> да --comment "…"');
if (s.questions.some((q) => q.options?.length)) {
out.push('Вопрос с вариантами — вариант надо назвать: autopasha answer <id> да --option "надпись"');
}
}

if (s.recent?.length) {
Expand Down
55 changes: 55 additions & 0 deletions test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,61 @@ test('status показывает состояние, вопросы и как
assert.match(text, /autopasha answer <id> да/);
});

test('status показывает варианты ответа и поле, без которых вопрос не ответить', async () => {
const env = await sandbox();
const out = collect();
const code = await run(['status'], {
env: { ...env, AUTOPASHA_TOKEN: 'ap_live_test' },
out: out.write,
err: out.write,
client: {
status: async () => ({
employee: { name: 'Паша', state: 'работает' },
waiting: { onOwner: 2, selfResolving: 0, total: 2 },
questions: [
{
id: 'q-1',
question: 'Куда писать?',
options: [
{ label: 'на общий ящик', decision: 'approved', hint: 'быстрее' },
{ label: 'искать личные', decision: 'approved' },
],
},
{ id: 'q-2', question: 'Код из СМС', input: { label: 'код из СМС', kind: 'number', required: true } },
],
recent: [],
}),
},
});
assert.equal(code, 0);
const text = out.text();
assert.match(text, /— на общий ящик \(быстрее\)/);
assert.match(text, /— искать личные/);
assert.match(text, /нужно заполнить: код из СМС/);
// Подсказка про вариант появляется только тогда, когда вариант где-то есть.
assert.match(text, /--option "надпись"/);
});

test('answer передаёт выбранный вариант, а решение по нему считает сервер', async () => {
const env = await sandbox();
const out = collect();
let got;
const code = await run(['answer', 'q-1', 'да', '--option', 'на общий ящик'], {
env: { ...env, AUTOPASHA_TOKEN: 'ap_live_test' },
out: out.write,
err: out.write,
client: {
answer: async (id, body) => {
got = { id, body };
return { id, decision: body.decision };
},
},
});
assert.equal(code, 0);
assert.deepEqual(got, { id: 'q-1', body: { decision: 'approved', option: 'на общий ящик' } });
assert.match(out.text(), /Ответ записан: да — на общий ящик/);
});

test('feed --since 2h отсекает старое на нашей стороне, курсор не выдумывает', async () => {
const env = await sandbox();
const out = collect();
Expand Down