codeply-cli 0.4.0 → 0.4.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/lib/cloud.mjs +97 -9
- package/package.json +1 -1
package/lib/cloud.mjs
CHANGED
|
@@ -495,9 +495,13 @@ export async function cloudRunStatus({ cwd, taskId, token, home = defaultHome(),
|
|
|
495
495
|
const checks = (await api('GET', `/repos/${task.repo}/commits/${run.head_sha}/check-runs?check_name=${encodeURIComponent(checkName(task.id))}`)).json;
|
|
496
496
|
const check = (checks.check_runs || [])[0];
|
|
497
497
|
if (check && check.output) next.progress = check.output.summary || '';
|
|
498
|
+
// The step-by-step record rides along to the caller but isn't kept in cloud.json.
|
|
499
|
+
let events = [];
|
|
500
|
+
let data = {};
|
|
501
|
+
try { data = JSON.parse((check && check.output && check.output.text) || '{}'); } catch {}
|
|
502
|
+
if (Array.isArray(data.events)) events = data.events;
|
|
498
503
|
if (check && check.status === 'completed') {
|
|
499
|
-
|
|
500
|
-
try { result = JSON.parse(check.output.text || '{}'); } catch {}
|
|
504
|
+
const { events: _drop, ...result } = data;
|
|
501
505
|
Object.assign(next, { status: check.conclusion === 'success' ? 'done' : 'failed', result, error: result.error || null, finishedAt: Date.now() });
|
|
502
506
|
} else if (run.status === 'completed') {
|
|
503
507
|
const why = run.conclusion === 'cancelled' ? 'The run was cancelled.' : (await diagnoseRun(api, task.repo, run.id)) || `The run ended (${run.conclusion}) before Craft could report back. The run log has details.`;
|
|
@@ -506,7 +510,7 @@ export async function cloudRunStatus({ cwd, taskId, token, home = defaultHome(),
|
|
|
506
510
|
next.status = run.status === 'queued' || run.status === 'waiting' || run.status === 'pending' ? 'queued' : 'running';
|
|
507
511
|
}
|
|
508
512
|
saveTask(cwd, home, next);
|
|
509
|
-
return next;
|
|
513
|
+
return { ...next, events };
|
|
510
514
|
}
|
|
511
515
|
|
|
512
516
|
/**
|
|
@@ -528,6 +532,7 @@ export async function importRemoteTasks({ cwd, token, home = defaultHome(), apiU
|
|
|
528
532
|
if (!check || check.status !== 'completed') continue;
|
|
529
533
|
let result = {};
|
|
530
534
|
try { result = JSON.parse(check.output.text || '{}'); } catch {}
|
|
535
|
+
delete result.events;
|
|
531
536
|
const task = {
|
|
532
537
|
id, prompt: result.prompt || '(started elsewhere)', mode: result.mode || 'Build', sessionId: result.sessionId || '', baseSha: result.baseSha || run.head_sha,
|
|
533
538
|
repo: p.repo, runId: run.id, runUrl: run.html_url, status: check.conclusion === 'success' ? 'done' : 'failed', result, error: result.error || null,
|
|
@@ -592,6 +597,71 @@ function renderProgress(lines, note) {
|
|
|
592
597
|
return `${note}\n\n${shown.join('\n')}`.slice(0, MAX_OUTPUT);
|
|
593
598
|
}
|
|
594
599
|
|
|
600
|
+
// ─── The run as a chat: every step the agent took, published live ─────────
|
|
601
|
+
// Cloud chats look like local ones (commands, edits with their + and - lines,
|
|
602
|
+
// narration, the "what actually happened" card), so the runner records the
|
|
603
|
+
// same events the desktop gets from a local run and puts them in the check
|
|
604
|
+
// run's text, where the PC and the phone read them while it works.
|
|
605
|
+
|
|
606
|
+
const EVENT_BUDGET = 58000;
|
|
607
|
+
const clipText = (s, n) => (typeof s === 'string' && s.length > n ? `${s.slice(0, n)}\n...` : s);
|
|
608
|
+
|
|
609
|
+
/** One agent event as a small record; tool arguments kept the way the chat shows them. */
|
|
610
|
+
export function recordEvent(ev) {
|
|
611
|
+
if (ev.type === 'text') return { t: 'text', text: clipText(ev.text, 8000), interim: !!ev.interim };
|
|
612
|
+
if (ev.type === 'reasoning') return { t: 'reasoning', text: clipText(ev.text, 2000), ms: ev.ms };
|
|
613
|
+
if (ev.type === 'notice') return { t: 'notice', level: ev.level || 'info', text: clipText(ev.text, 1000) };
|
|
614
|
+
if (ev.type === 'tool_end') {
|
|
615
|
+
const a = ev.args || {};
|
|
616
|
+
let args;
|
|
617
|
+
if (ev.name === 'write_file') args = { path: a.path };
|
|
618
|
+
else if (ev.name === 'apply_patch') args = { files: (ev.meta && ev.meta.files) || [] };
|
|
619
|
+
else {
|
|
620
|
+
args = {};
|
|
621
|
+
for (const [k, v] of Object.entries(a)) {
|
|
622
|
+
if (typeof v === 'string') args[k] = clipText(v, k === 'search' || k === 'replace' ? 3000 : 1500);
|
|
623
|
+
else if (typeof v === 'number' || typeof v === 'boolean') args[k] = v;
|
|
624
|
+
else if (Array.isArray(v)) args[k] = v.slice(0, 50);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const m = ev.meta || {};
|
|
628
|
+
return {
|
|
629
|
+
t: 'tool', name: ev.name, ok: ev.ok, summary: clipText(ev.summary || '', 300), args,
|
|
630
|
+
...(typeof m.exitCode === 'number' ? { exitCode: m.exitCode } : {}),
|
|
631
|
+
...(typeof m.added === 'number' ? { added: m.added } : {}),
|
|
632
|
+
...(typeof m.removed === 'number' ? { removed: m.removed } : {}),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
if (ev.type === 'done' && Array.isArray(ev.actions)) {
|
|
636
|
+
const changed = ev.actions.filter((x) => x.ok && ['write_file', 'edit_file', 'apply_patch', 'fetch_image'].includes(x.tool))
|
|
637
|
+
.flatMap((x) => (Array.isArray(x.files) && x.files.length ? x.files.map((f) => ({ ...x, label: f })) : [x]));
|
|
638
|
+
const checks = ev.actions.filter((x) => ['run', 'browser_check'].includes(x.tool));
|
|
639
|
+
if (!changed.length && !checks.length) return null;
|
|
640
|
+
return {
|
|
641
|
+
t: 'summary', files: [...new Set(changed.map((x) => x.label))].slice(0, 30),
|
|
642
|
+
checks: [...new Map(checks.map((x) => [`${x.tool}|${x.label}`, x])).values()].slice(-8).map((x) => ({ tool: x.tool, label: x.label, ok: x.ok, exitCode: x.exitCode })),
|
|
643
|
+
unverified: ev.unverifiedFiles || [],
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Fit the event list in the check run's text: shorten diffs, drop thinking, then the oldest steps. */
|
|
650
|
+
export function packEvents(events, budget = EVENT_BUDGET) {
|
|
651
|
+
let list = events.map((e) => ({ ...e }));
|
|
652
|
+
const size = () => JSON.stringify(list).length;
|
|
653
|
+
if (size() <= budget) return list;
|
|
654
|
+
list = list.map((e) => (e.t === 'reasoning' ? { ...e, text: clipText(e.text, 200) } : e));
|
|
655
|
+
for (const n of [800, 200]) {
|
|
656
|
+
if (size() <= budget) return list;
|
|
657
|
+
list = list.map((e) => (e.t === 'tool' && e.args ? { ...e, args: Object.fromEntries(Object.entries(e.args).map(([k, v]) => [k, typeof v === 'string' ? clipText(v, n) : v])) } : e));
|
|
658
|
+
}
|
|
659
|
+
let dropped = 0;
|
|
660
|
+
while (size() > budget && list.length > 1) { list.shift(); dropped++; }
|
|
661
|
+
if (dropped) list.unshift({ t: 'notice', level: 'info', text: `${dropped} earlier step${dropped === 1 ? '' : 's'} are only in the run log.` });
|
|
662
|
+
return list;
|
|
663
|
+
}
|
|
664
|
+
|
|
595
665
|
async function loadHistory(api, repo, sessionId) {
|
|
596
666
|
if (!sessionId) return { messages: [], sha: null };
|
|
597
667
|
const r = await api('GET', `/repos/${repo}/contents/sessions/${encodeURIComponent(sessionId)}.json?ref=${SESSIONS_BRANCH}`, null, { allow: [404] });
|
|
@@ -635,12 +705,16 @@ export async function runCloudRunner({ env = process.env, cwd = process.cwd(), t
|
|
|
635
705
|
name: checkName(taskId), head_sha: headSha, status: 'in_progress', started_at: new Date().toISOString(),
|
|
636
706
|
output: { title: 'Starting', summary: 'Starting...' },
|
|
637
707
|
})).json;
|
|
708
|
+
const events = [];
|
|
638
709
|
const finish = async (ok, title, summary, result) => {
|
|
710
|
+
// The result's fields stay at the top level (older apps read them there); events share the space left.
|
|
711
|
+
const base = JSON.stringify(result);
|
|
712
|
+
const packed = packEvents(events, Math.max(4000, MAX_OUTPUT - base.length - 200));
|
|
639
713
|
await api('PATCH', `/repos/${repo}/check-runs/${check.id}`, {
|
|
640
714
|
status: 'completed', conclusion: ok ? 'success' : 'failure', completed_at: new Date().toISOString(),
|
|
641
|
-
output: { title, summary: String(summary || title).slice(0, MAX_OUTPUT), text: JSON.stringify(result).slice(0, MAX_OUTPUT) },
|
|
715
|
+
output: { title, summary: String(summary || title).slice(0, MAX_OUTPUT), text: JSON.stringify({ ...result, events: packed }).slice(0, MAX_OUTPUT) },
|
|
642
716
|
}).catch((e) => log(`Could not finish the check run: ${e.message}`));
|
|
643
|
-
return { status: ok ? 'done' : 'failed', result };
|
|
717
|
+
return { status: ok ? 'done' : 'failed', result: { ...result, events: packed } };
|
|
644
718
|
};
|
|
645
719
|
|
|
646
720
|
if (setupError) return finish(false, 'Could not start', setupError, { error: setupError, mode });
|
|
@@ -651,7 +725,9 @@ export async function runCloudRunner({ env = process.env, cwd = process.cwd(), t
|
|
|
651
725
|
const pushProgress = async (note, force = false) => {
|
|
652
726
|
if (!force && Date.now() - lastPush < updateMs) return;
|
|
653
727
|
lastPush = Date.now();
|
|
654
|
-
pending = api('PATCH', `/repos/${repo}/check-runs/${check.id}`, {
|
|
728
|
+
pending = api('PATCH', `/repos/${repo}/check-runs/${check.id}`, {
|
|
729
|
+
output: { title: 'Working', summary: renderProgress(lines, note), text: JSON.stringify({ running: true, events: packEvents(events) }) },
|
|
730
|
+
}).catch(() => {});
|
|
655
731
|
await pending;
|
|
656
732
|
};
|
|
657
733
|
|
|
@@ -662,9 +738,13 @@ export async function runCloudRunner({ env = process.env, cwd = process.cwd(), t
|
|
|
662
738
|
let answer = ''; let steps = 0; let failure = '';
|
|
663
739
|
const userMessage = `${prompt}\n\n(You are running unattended in Craft Cloud, on a copy of the project. Nobody can answer questions mid-run, so make sensible choices and say what you assumed. Do not commit or push; Craft does that when you finish.)`;
|
|
664
740
|
for await (const ev of runAgentImpl({ userMessage, history: hist.messages.slice(-HISTORY_TURNS), mode, cwd, approve, signal: new AbortController().signal, route, maxSteps })) {
|
|
741
|
+
const rec = recordEvent(ev);
|
|
742
|
+
// The final answer arrives as the reply; everything else is part of the chat's steps.
|
|
743
|
+
if (rec && !(rec.t === 'text' && !rec.interim)) events.push(rec);
|
|
665
744
|
if (ev.type === 'text' && !ev.interim) answer = ev.text;
|
|
666
|
-
else if (ev.type === 'tool_end') { steps++; lines.push(describeTool(ev));
|
|
745
|
+
else if (ev.type === 'tool_end') { steps++; lines.push(describeTool(ev)); }
|
|
667
746
|
else if (ev.type === 'error') failure = ev.error;
|
|
747
|
+
if (rec) await pushProgress(`Working: ${steps} step${steps === 1 ? '' : 's'} so far.`);
|
|
668
748
|
if (ev.type === 'done' || ev.type === 'error' || ev.type === 'aborted') break;
|
|
669
749
|
}
|
|
670
750
|
if (pending) await pending;
|
|
@@ -676,22 +756,30 @@ export async function runCloudRunner({ env = process.env, cwd = process.cwd(), t
|
|
|
676
756
|
if (files.length) answer = fs.readFileSync(path.join(dir, files[0].f), 'utf8');
|
|
677
757
|
}
|
|
678
758
|
|
|
679
|
-
let branch = null; let sha = null; let files = [];
|
|
759
|
+
let branch = null; let sha = null; let files = []; let stats = [];
|
|
680
760
|
if (mode === 'Build') {
|
|
681
761
|
await git(cwd, ['add', '-A', '--', '.', ':(exclude).codeply', `:(exclude)${WORKFLOW_PATH}`]);
|
|
682
762
|
const staged = await git(cwd, ['diff', '--cached', '--name-only']);
|
|
683
763
|
if (staged) {
|
|
684
764
|
files = staged.split('\n').filter(Boolean);
|
|
765
|
+
// Per-file + and - lines, for the chat's record of what was pushed.
|
|
766
|
+
stats = (await git(cwd, ['diff', '--cached', '--numstat'])).split('\n').filter(Boolean).map((l) => {
|
|
767
|
+
const [add, del, ...name] = l.split('\t');
|
|
768
|
+
return { file: name.join('\t'), added: add === '-' ? null : Number(add), removed: del === '-' ? null : Number(del) };
|
|
769
|
+
});
|
|
685
770
|
branch = taskBranch(taskId);
|
|
686
771
|
await git(cwd, [...BOT, 'commit', '-q', '-m', `${prompt.split('\n')[0].slice(0, 60)}\n\nCraft cloud task ${taskId}.`]);
|
|
687
772
|
sha = await git(cwd, ['rev-parse', 'HEAD']);
|
|
688
773
|
await git(cwd, ['push', '-q', 'origin', `HEAD:refs/heads/${branch}`], { auth: authFor(token, serverUrl) });
|
|
774
|
+
const plus = stats.reduce((n, s) => n + (s.added || 0), 0);
|
|
775
|
+
const minus = stats.reduce((n, s) => n + (s.removed || 0), 0);
|
|
776
|
+
events.push({ t: 'pushed', branch, sha: sha.slice(0, 7), files: stats.slice(0, 50), added: plus, removed: minus });
|
|
689
777
|
}
|
|
690
778
|
}
|
|
691
779
|
|
|
692
780
|
await saveHistory(api, repo, sessionId, [...hist.messages, { role: 'user', content: prompt }, { role: 'assistant', content: answer || '(no reply)' }].slice(-HISTORY_TURNS * 2), headSha)
|
|
693
781
|
.catch((e) => log(`Could not save the chat: ${e.message}`));
|
|
694
|
-
const result = { mode, prompt: prompt.slice(0, 4000), sessionId, answer: answer.slice(0,
|
|
782
|
+
const result = { mode, prompt: prompt.slice(0, 4000), sessionId, answer: answer.slice(0, 20000), branch, sha, files, stats: stats.slice(0, 100), steps, baseSha: headSha };
|
|
695
783
|
const title = files.length ? `Changed ${files.length} file${files.length === 1 ? '' : 's'}` : mode === 'Build' ? 'No files changed' : 'Answered';
|
|
696
784
|
return finish(true, title, answer || title, result);
|
|
697
785
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeply-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Codeply from the terminal: an interactive AI coding agent, a local server (codeply serve), and a GitHub agent that runs in your own Actions runner.",
|
|
5
5
|
"main": "bin/codeply.js",
|
|
6
6
|
"bin": {
|