orizu 0.5.6 → 0.5.8
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/README.md +13 -2
- package/dist/credentials.js +44 -0
- package/dist/help.js +77 -7
- package/dist/hosted-bootstrap.js +39 -0
- package/dist/hosted-harness-opencode.js +21 -1
- package/dist/hosted-harness.js +9 -0
- package/dist/hosted-harvest.js +116 -0
- package/dist/hosted-headless.js +163 -0
- package/dist/hosted-loop.js +119 -5
- package/dist/hosted-run-event-sink.js +21 -0
- package/dist/hosted-runtime-assets.js +27 -0
- package/dist/hosted-session-cli.js +51 -9
- package/dist/http.js +16 -1
- package/dist/index.js +54 -86
- package/dist/manifests-cli.js +103 -5
- package/dist/report-comments-cli.js +325 -0
- package/dist/skill-installer.js +16 -1
- package/package.json +1 -1
- package/src/credentials.ts +46 -0
- package/src/help.ts +77 -7
- package/src/hosted-bootstrap.ts +46 -0
- package/src/hosted-harness-opencode.ts +24 -2
- package/src/hosted-harness.ts +9 -0
- package/src/hosted-harvest.ts +159 -0
- package/src/hosted-headless.ts +178 -0
- package/src/hosted-loop.ts +133 -4
- package/src/hosted-run-event-sink.ts +28 -0
- package/src/hosted-runtime-assets.ts +31 -0
- package/src/hosted-session-cli.ts +50 -9
- package/src/http.ts +23 -1
- package/src/index.ts +74 -156
- package/src/manifests-cli.ts +110 -5
- package/src/report-comments-cli.ts +524 -0
- package/src/skill-installer.ts +18 -1
- package/vendor/skills/orizu-cli/SKILL.md +9 -2
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +1 -1
package/dist/manifests-cli.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* server — re-applying returns the stored outcome.
|
|
6
6
|
*/
|
|
7
7
|
import { authedFetch } from './http.js';
|
|
8
|
-
const MANIFEST_ACTIONS = new Set(['approve', 'reject', 'apply']);
|
|
8
|
+
const MANIFEST_ACTIONS = new Set(['approve', 'reject', 'apply', 'revert']);
|
|
9
9
|
async function responseMessage(response) {
|
|
10
10
|
try {
|
|
11
11
|
const data = (await response.clone().json());
|
|
@@ -73,6 +73,86 @@ function asRecord(value) {
|
|
|
73
73
|
function num(value) {
|
|
74
74
|
return typeof value === 'number' ? value : 0;
|
|
75
75
|
}
|
|
76
|
+
// Auto-applied manifests (ALI-1040) landed by SERVER POLICY, not a human approver
|
|
77
|
+
// (approved_by is NULL). Mark them so a reviewer can tell policy applies from
|
|
78
|
+
// human applies at a glance; the reason lives in the outcome for `show`.
|
|
79
|
+
function wasAutoApplied(manifest) {
|
|
80
|
+
return asRecord(manifest.outcome).autoApplied === true;
|
|
81
|
+
}
|
|
82
|
+
/** STATUS cell: `applied*` when the server auto-applied it, else the raw status. */
|
|
83
|
+
function statusDisplay(manifest) {
|
|
84
|
+
const status = typeof manifest.status === 'string' ? manifest.status : '(unknown)';
|
|
85
|
+
return wasAutoApplied(manifest) ? `${status}*` : status;
|
|
86
|
+
}
|
|
87
|
+
/** One-line auto-apply audit note for `show`, or null when not auto-applied. */
|
|
88
|
+
function autoApplyNote(manifest) {
|
|
89
|
+
if (!wasAutoApplied(manifest))
|
|
90
|
+
return null;
|
|
91
|
+
const reason = asRecord(manifest.outcome).autoApplyReason;
|
|
92
|
+
return typeof reason === 'string' && reason.length > 0
|
|
93
|
+
? `auto-applied by policy: ${reason}`
|
|
94
|
+
: 'auto-applied by policy';
|
|
95
|
+
}
|
|
96
|
+
// -- list table (ALI-1038) ----------------------------------------------------
|
|
97
|
+
/** "3m" / "4h" / "2d" — enough resolution to pick a manifest out of a list. */
|
|
98
|
+
function relativeAge(iso, now) {
|
|
99
|
+
if (typeof iso !== 'string')
|
|
100
|
+
return '-';
|
|
101
|
+
const then = Date.parse(iso);
|
|
102
|
+
if (Number.isNaN(then))
|
|
103
|
+
return '-';
|
|
104
|
+
const mins = Math.max(0, Math.floor((now - then) / 60_000));
|
|
105
|
+
if (mins < 60)
|
|
106
|
+
return `${mins}m`;
|
|
107
|
+
const hours = Math.floor(mins / 60);
|
|
108
|
+
if (hours < 48)
|
|
109
|
+
return `${hours}h`;
|
|
110
|
+
return `${Math.floor(hours / 24)}d`;
|
|
111
|
+
}
|
|
112
|
+
/** One-glance change summary from the stored proposedState — no live API call. */
|
|
113
|
+
function changesSummary(manifest) {
|
|
114
|
+
if (manifest.actionType !== 'repo_merge')
|
|
115
|
+
return '-';
|
|
116
|
+
const proposed = asRecord(manifest.proposedState);
|
|
117
|
+
const files = num(proposed.filesChanged);
|
|
118
|
+
const parts = `${files} file${files === 1 ? '' : 's'} +${num(proposed.additions)}/-${num(proposed.deletions)}`;
|
|
119
|
+
// Instruction files steer agent behavior — reviewers should see the flag in the list.
|
|
120
|
+
return proposed.touches_instruction_files === true ? `${parts} !instr` : parts;
|
|
121
|
+
}
|
|
122
|
+
/** Reviewable-first (pending_approval, then draft), newest first within a group. */
|
|
123
|
+
function listOrder(a, b) {
|
|
124
|
+
const rank = (m) => m.status === 'pending_approval' ? 0 : m.status === 'draft' ? 1 : 2;
|
|
125
|
+
const byRank = rank(a) - rank(b);
|
|
126
|
+
if (byRank !== 0)
|
|
127
|
+
return byRank;
|
|
128
|
+
const created = (m) => typeof m.createdAt === 'string' ? Date.parse(m.createdAt) || 0 : 0;
|
|
129
|
+
return created(b) - created(a);
|
|
130
|
+
}
|
|
131
|
+
/** Render manifests as the standard padEnd table (matches teams/projects list). */
|
|
132
|
+
function manifestsTable(manifests, now) {
|
|
133
|
+
if (manifests.length === 0)
|
|
134
|
+
return 'promotion manifests\n(none)';
|
|
135
|
+
const rows = [...manifests].sort(listOrder).map(manifest => ({
|
|
136
|
+
id: typeof manifest.id === 'string' ? manifest.id : '(unknown)',
|
|
137
|
+
status: statusDisplay(manifest),
|
|
138
|
+
type: typeof manifest.actionType === 'string' ? manifest.actionType : '(unknown)',
|
|
139
|
+
changes: changesSummary(manifest),
|
|
140
|
+
author: typeof manifest.authorActorType === 'string' ? manifest.authorActorType : '-',
|
|
141
|
+
age: relativeAge(manifest.createdAt, now),
|
|
142
|
+
}));
|
|
143
|
+
const idWidth = Math.max('ID'.length, ...rows.map(row => row.id.length));
|
|
144
|
+
const statusWidth = Math.max('STATUS'.length, ...rows.map(row => row.status.length));
|
|
145
|
+
const typeWidth = Math.max('TYPE'.length, ...rows.map(row => row.type.length));
|
|
146
|
+
const changesWidth = Math.max('CHANGES'.length, ...rows.map(row => row.changes.length));
|
|
147
|
+
const authorWidth = Math.max('AUTHOR'.length, ...rows.map(row => row.author.length));
|
|
148
|
+
const header = `${'ID'.padEnd(idWidth)} ${'STATUS'.padEnd(statusWidth)} ${'TYPE'.padEnd(typeWidth)} ` +
|
|
149
|
+
`${'CHANGES'.padEnd(changesWidth)} ${'AUTHOR'.padEnd(authorWidth)} AGE`;
|
|
150
|
+
const divider = `${'-'.repeat(idWidth)} ${'-'.repeat(statusWidth)} ${'-'.repeat(typeWidth)} ` +
|
|
151
|
+
`${'-'.repeat(changesWidth)} ${'-'.repeat(authorWidth)} ---`;
|
|
152
|
+
const body = rows.map(row => `${row.id.padEnd(idWidth)} ${row.status.padEnd(statusWidth)} ${row.type.padEnd(typeWidth)} ` +
|
|
153
|
+
`${row.changes.padEnd(changesWidth)} ${row.author.padEnd(authorWidth)} ${row.age}`);
|
|
154
|
+
return [header, divider, ...body].join('\n');
|
|
155
|
+
}
|
|
76
156
|
// repo_merge manifests (ALI-972) render the compare summary from stored evidence
|
|
77
157
|
// — no live GitHub call — so a reviewer can approve informed without a PR.
|
|
78
158
|
function repoMergeShow(manifest) {
|
|
@@ -108,7 +188,7 @@ export async function manifestsCommand(args, io) {
|
|
|
108
188
|
const response = await fetcherFrom(io)(`/api/cli/promotion-manifests${query}`);
|
|
109
189
|
const data = await requireOk(response, 'Manifest list');
|
|
110
190
|
const manifests = Array.isArray(data.manifests) ? data.manifests : [];
|
|
111
|
-
emit(io, { manifests },
|
|
191
|
+
emit(io, { manifests }, manifestsTable(manifests, Date.now()));
|
|
112
192
|
return 0;
|
|
113
193
|
}
|
|
114
194
|
if (subcommand === 'show') {
|
|
@@ -119,7 +199,9 @@ export async function manifestsCommand(args, io) {
|
|
|
119
199
|
const response = await fetcherFrom(io)(`/api/cli/promotion-manifests/${encodeURIComponent(id)}`);
|
|
120
200
|
const data = await requireOk(response, 'Manifest show');
|
|
121
201
|
const manifest = (data.manifest ?? {});
|
|
122
|
-
const
|
|
202
|
+
const base = manifest.actionType === 'repo_merge' ? repoMergeShow(manifest) : manifestLine(manifest);
|
|
203
|
+
const note = autoApplyNote(manifest);
|
|
204
|
+
const human = note ? `${base}\n ${note}` : base;
|
|
123
205
|
emit(io, { manifest }, human);
|
|
124
206
|
return 0;
|
|
125
207
|
}
|
|
@@ -135,9 +217,25 @@ export async function manifestsCommand(args, io) {
|
|
|
135
217
|
});
|
|
136
218
|
const data = await requireOk(response, `Manifest ${subcommand}`);
|
|
137
219
|
const manifest = (data.manifest ?? {});
|
|
138
|
-
|
|
220
|
+
const lines = [manifestLine(manifest)];
|
|
221
|
+
// Reject reaps the session branch best-effort; a failed reap must never be
|
|
222
|
+
// silent (ALI-1043) — surface it with the exact cleanup command.
|
|
223
|
+
if (subcommand === 'reject' && manifest.actionType === 'repo_merge') {
|
|
224
|
+
const outcome = asRecord(manifest.outcome);
|
|
225
|
+
const branch = asRecord(manifest.proposedState).branch;
|
|
226
|
+
if (outcome.branchDeleted === true) {
|
|
227
|
+
lines.push('session branch deleted');
|
|
228
|
+
}
|
|
229
|
+
else if (typeof branch === 'string' && branch.length > 0) {
|
|
230
|
+
lines.push(`warning: the session branch could not be deleted — remove it with: git push origin --delete ${branch}`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
lines.push('warning: the session branch could not be deleted (branch name unavailable)');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
emit(io, { manifest }, lines.join('\n'));
|
|
139
237
|
return 0;
|
|
140
238
|
}
|
|
141
|
-
io.print('Usage: orizu manifests <list|show|approve|reject|apply> ... [--json]');
|
|
239
|
+
io.print('Usage: orizu manifests <list|show|approve|reject|apply|revert> ... [--json]');
|
|
142
240
|
return 1;
|
|
143
241
|
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
const COMMENTS_TARGET_USAGE = 'Use exactly one of --prompt <id-or-name>, --run <run-id>, or --task <task-id>';
|
|
2
|
+
function reportCommentsUsage(action) {
|
|
3
|
+
const target = '(--prompt <id-or-name> --project <team/project> [--label <label> | --version <id>] | --run <run-id> | --task <task-id>)';
|
|
4
|
+
if (action === 'list') {
|
|
5
|
+
return `Usage: orizu comments list ${target} [--json]`;
|
|
6
|
+
}
|
|
7
|
+
if (action === 'add') {
|
|
8
|
+
return `Usage: orizu comments add ${target} --body <text|@file> [--anchor <text>] [--lines <start:end>] [--via <name>] [--json]`;
|
|
9
|
+
}
|
|
10
|
+
if (action === 'reply') {
|
|
11
|
+
return 'Usage: orizu comments reply <comment-id> --body <text|@file> [--via <name>] [--json]';
|
|
12
|
+
}
|
|
13
|
+
if (action === 'edit') {
|
|
14
|
+
return 'Usage: orizu comments edit <comment-id> --body <text|@file> [--json]';
|
|
15
|
+
}
|
|
16
|
+
return `Usage: orizu comments ${action} <comment-id> [--json]`;
|
|
17
|
+
}
|
|
18
|
+
function compactTerminalText(ctx, value, maxLength = 160) {
|
|
19
|
+
const compact = ctx.sanitizeTerminalText(value).replace(/\s+/g, ' ').trim();
|
|
20
|
+
if (compact.length <= maxLength) {
|
|
21
|
+
return compact;
|
|
22
|
+
}
|
|
23
|
+
return `${compact.slice(0, Math.max(0, maxLength - 1)).trim()}…`;
|
|
24
|
+
}
|
|
25
|
+
async function readReportCommentError(ctx, response) {
|
|
26
|
+
const status = response.status
|
|
27
|
+
? `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`
|
|
28
|
+
: 'request failed';
|
|
29
|
+
const contentType = response.headers.get('content-type') || '';
|
|
30
|
+
if (!contentType.toLowerCase().includes('application/json')) {
|
|
31
|
+
return status;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const parsed = await response.clone().json();
|
|
35
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
36
|
+
const error = parsed.error;
|
|
37
|
+
if (typeof error === 'string' && error.trim()) {
|
|
38
|
+
return compactTerminalText(ctx, error, 220);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Fall through to the status-only message for malformed JSON errors.
|
|
44
|
+
}
|
|
45
|
+
return status;
|
|
46
|
+
}
|
|
47
|
+
function printIndentedBody(ctx, value, indent) {
|
|
48
|
+
const lines = ctx.sanitizeTerminalText(value || '').split(/\r?\n/);
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
ctx.printLine(`${indent}${line}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function formatReportCommentAnchor(ctx, anchor) {
|
|
54
|
+
if (!anchor) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const lineLabel = anchor.startLine && anchor.endLine
|
|
58
|
+
? anchor.startLine === anchor.endLine
|
|
59
|
+
? `line ${anchor.startLine}`
|
|
60
|
+
: `lines ${anchor.startLine}-${anchor.endLine}`
|
|
61
|
+
: null;
|
|
62
|
+
const selectedText = anchor.text ? `"${compactTerminalText(ctx, anchor.text, 180)}"` : null;
|
|
63
|
+
if (lineLabel && selectedText) {
|
|
64
|
+
return `${lineLabel}: ${selectedText}`;
|
|
65
|
+
}
|
|
66
|
+
return lineLabel || selectedText;
|
|
67
|
+
}
|
|
68
|
+
function reportCommentAuthorName(author) {
|
|
69
|
+
const personName = author?.person?.name;
|
|
70
|
+
const name = personName || author?.name || 'Unknown';
|
|
71
|
+
const via = author?.via ? ` via ${author.via}` : '';
|
|
72
|
+
const agent = author?.actor === 'agent'
|
|
73
|
+
? author.onBehalfOf
|
|
74
|
+
? ` (agent on behalf of ${author.onBehalfOf})`
|
|
75
|
+
: ' (agent)'
|
|
76
|
+
: '';
|
|
77
|
+
return `${name}${via}${agent}`;
|
|
78
|
+
}
|
|
79
|
+
function printReportCommentSubject(ctx, data) {
|
|
80
|
+
if (data.prompt && data.version) {
|
|
81
|
+
const versionLabel = data.version.versionLabel || (data.version.versionNumber ? `v${data.version.versionNumber}` : data.version.id);
|
|
82
|
+
ctx.printLine(`Prompt: ${ctx.sanitizeTerminalText(data.prompt.name)} (${ctx.sanitizeTerminalText(data.prompt.id)})`);
|
|
83
|
+
ctx.printLine(`Version: ${ctx.sanitizeTerminalText(String(versionLabel))} (${ctx.sanitizeTerminalText(data.version.id)})`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (data.run) {
|
|
87
|
+
ctx.printLine(`Optimization run: ${ctx.sanitizeTerminalText(data.run.id)}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (data.task) {
|
|
91
|
+
const title = data.task.title ? `${ctx.sanitizeTerminalText(data.task.title)} ` : '';
|
|
92
|
+
ctx.printLine(`Task: ${title}(${ctx.sanitizeTerminalText(data.task.id)})`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (data.subject) {
|
|
96
|
+
ctx.printLine(`Subject: ${ctx.sanitizeTerminalText(data.subject.type)} ${ctx.sanitizeTerminalText(data.subject.id)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function printReportComments(ctx, data) {
|
|
100
|
+
printReportCommentSubject(ctx, data);
|
|
101
|
+
ctx.printLine(`Comments: ${data.summary.threadCount} thread${data.summary.threadCount === 1 ? '' : 's'} ` +
|
|
102
|
+
`(${data.summary.openThreadCount} open, ${data.summary.resolvedThreadCount} resolved, ${data.summary.replyCount} replies)`);
|
|
103
|
+
if (data.comments.length === 0) {
|
|
104
|
+
ctx.printLine('\nNo comments found for this report.');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (const [index, thread] of data.comments.entries()) {
|
|
108
|
+
const authorName = reportCommentAuthorName(thread.author);
|
|
109
|
+
const anchor = formatReportCommentAnchor(ctx, thread.anchor);
|
|
110
|
+
ctx.printLine('');
|
|
111
|
+
ctx.printLine(`${index + 1}. [${thread.status}] ${ctx.sanitizeTerminalText(thread.id)} · ` +
|
|
112
|
+
`${ctx.sanitizeTerminalText(authorName)} · ${ctx.sanitizeTerminalText(thread.createdAt)}`);
|
|
113
|
+
if (thread.editedAt) {
|
|
114
|
+
ctx.printLine(` Edited: ${ctx.sanitizeTerminalText(thread.editedAt)}`);
|
|
115
|
+
}
|
|
116
|
+
if (anchor) {
|
|
117
|
+
ctx.printLine(` Selection: ${anchor}`);
|
|
118
|
+
}
|
|
119
|
+
printIndentedBody(ctx, thread.body, ' ');
|
|
120
|
+
if (thread.replies.length > 0) {
|
|
121
|
+
ctx.printLine(` Replies (${thread.replies.length})`);
|
|
122
|
+
for (const reply of thread.replies) {
|
|
123
|
+
const replyAuthor = reportCommentAuthorName(reply.author);
|
|
124
|
+
ctx.printLine(` - ${ctx.sanitizeTerminalText(reply.id)} · ` +
|
|
125
|
+
`${ctx.sanitizeTerminalText(replyAuthor)} · ${ctx.sanitizeTerminalText(reply.createdAt)}`);
|
|
126
|
+
if (reply.editedAt) {
|
|
127
|
+
ctx.printLine(` Edited: ${ctx.sanitizeTerminalText(reply.editedAt)}`);
|
|
128
|
+
}
|
|
129
|
+
printIndentedBody(ctx, reply.body, ' ');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function reportCommentsTargetParams(ctx, action) {
|
|
135
|
+
const promptRef = ctx.getArg('--prompt');
|
|
136
|
+
const runId = ctx.getArg('--run') || ctx.getArg('--run-id');
|
|
137
|
+
const taskId = ctx.getArg('--task');
|
|
138
|
+
ctx.rejectDashPrefixedOptionValue('--prompt', promptRef);
|
|
139
|
+
ctx.rejectDashPrefixedOptionValue('--run', runId);
|
|
140
|
+
ctx.rejectDashPrefixedOptionValue('--task', taskId);
|
|
141
|
+
const present = [
|
|
142
|
+
promptRef ? 'prompt' : null,
|
|
143
|
+
runId ? 'run' : null,
|
|
144
|
+
taskId ? 'task' : null,
|
|
145
|
+
].filter(Boolean);
|
|
146
|
+
if (present.length !== 1) {
|
|
147
|
+
throw new Error(`${reportCommentsUsage(action)}\n${COMMENTS_TARGET_USAGE}`);
|
|
148
|
+
}
|
|
149
|
+
const params = new URLSearchParams();
|
|
150
|
+
if (promptRef) {
|
|
151
|
+
const project = ctx.getArg('--project') || await ctx.resolveProjectSlug(null);
|
|
152
|
+
const label = ctx.getArg('--label');
|
|
153
|
+
const version = ctx.getArg('--version');
|
|
154
|
+
ctx.rejectDashPrefixedOptionValue('--project', project);
|
|
155
|
+
ctx.rejectDashPrefixedOptionValue('--label', label);
|
|
156
|
+
ctx.rejectDashPrefixedOptionValue('--version', version);
|
|
157
|
+
if (label && version) {
|
|
158
|
+
throw new Error('Use either --label or --version, not both');
|
|
159
|
+
}
|
|
160
|
+
params.set('prompt', promptRef);
|
|
161
|
+
params.set('project', project);
|
|
162
|
+
if (label)
|
|
163
|
+
params.set('label', label);
|
|
164
|
+
if (version)
|
|
165
|
+
params.set('version', version);
|
|
166
|
+
return params;
|
|
167
|
+
}
|
|
168
|
+
if (runId) {
|
|
169
|
+
params.set('run', runId);
|
|
170
|
+
return params;
|
|
171
|
+
}
|
|
172
|
+
params.set('task', taskId);
|
|
173
|
+
return params;
|
|
174
|
+
}
|
|
175
|
+
function readReportCommentBodyInput(ctx, action) {
|
|
176
|
+
const body = ctx.getArg('--body');
|
|
177
|
+
ctx.rejectDashPrefixedOptionValue('--body', body);
|
|
178
|
+
if (!body) {
|
|
179
|
+
throw new Error(reportCommentsUsage(action));
|
|
180
|
+
}
|
|
181
|
+
if (body === '@') {
|
|
182
|
+
throw new Error('Usage: --body <text|@file>');
|
|
183
|
+
}
|
|
184
|
+
const value = body.startsWith('@') ? ctx.readSourceFile(body.slice(1)) : body;
|
|
185
|
+
if (!value.trim()) {
|
|
186
|
+
throw new Error('Comment body must not be blank');
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
function readReportCommentAnchorInput(ctx) {
|
|
191
|
+
const anchor = ctx.getArg('--anchor');
|
|
192
|
+
const lines = ctx.getArg('--lines');
|
|
193
|
+
ctx.rejectDashPrefixedOptionValue('--anchor', anchor);
|
|
194
|
+
ctx.rejectDashPrefixedOptionValue('--lines', lines);
|
|
195
|
+
let startLine = null;
|
|
196
|
+
let endLine = null;
|
|
197
|
+
if (lines) {
|
|
198
|
+
const match = lines.match(/^(\d+):(\d+)$/);
|
|
199
|
+
if (!match) {
|
|
200
|
+
throw new Error('--lines must use format <start:end>');
|
|
201
|
+
}
|
|
202
|
+
startLine = Number(match[1]);
|
|
203
|
+
endLine = Number(match[2]);
|
|
204
|
+
if (!Number.isInteger(startLine) || !Number.isInteger(endLine) || startLine < 1 || endLine < startLine) {
|
|
205
|
+
throw new Error('--lines must contain positive integers where end is greater than or equal to start');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!anchor && startLine === null) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
text: anchor ? anchor.trim() : null,
|
|
213
|
+
startLine,
|
|
214
|
+
endLine,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function readReportCommentViaInput(ctx) {
|
|
218
|
+
const via = ctx.getArg('--via');
|
|
219
|
+
ctx.rejectDashPrefixedOptionValue('--via', via);
|
|
220
|
+
return via?.trim() || undefined;
|
|
221
|
+
}
|
|
222
|
+
async function listReportComments(ctx) {
|
|
223
|
+
const params = await reportCommentsTargetParams(ctx, 'list');
|
|
224
|
+
const response = await ctx.authedFetch(`/api/cli/report-comments?${params.toString()}`);
|
|
225
|
+
if (!response.ok) {
|
|
226
|
+
throw new Error(`Failed to fetch report comments: ${await readReportCommentError(ctx, response)}`);
|
|
227
|
+
}
|
|
228
|
+
const data = await ctx.parseJsonResponse(response, 'Report comments');
|
|
229
|
+
if (ctx.hasJsonFlag()) {
|
|
230
|
+
ctx.printJson(data);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
printReportComments(ctx, data);
|
|
234
|
+
}
|
|
235
|
+
async function addReportComment(ctx) {
|
|
236
|
+
const params = await reportCommentsTargetParams(ctx, 'add');
|
|
237
|
+
const body = readReportCommentBodyInput(ctx, 'add');
|
|
238
|
+
const response = await ctx.authedFetch(`/api/cli/report-comments?${params.toString()}`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: { 'Content-Type': 'application/json' },
|
|
241
|
+
body: JSON.stringify({
|
|
242
|
+
body,
|
|
243
|
+
via: readReportCommentViaInput(ctx),
|
|
244
|
+
anchor: readReportCommentAnchorInput(ctx),
|
|
245
|
+
}),
|
|
246
|
+
});
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
throw new Error(`Failed to add report comment: ${await readReportCommentError(ctx, response)}`);
|
|
249
|
+
}
|
|
250
|
+
const data = await ctx.parseJsonResponse(response, 'Report comment add');
|
|
251
|
+
if (ctx.hasJsonFlag()) {
|
|
252
|
+
ctx.printJson(data);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
ctx.printLine(`Added comment ${ctx.sanitizeTerminalText(data.comment.id)}`);
|
|
256
|
+
}
|
|
257
|
+
async function replyReportComment(ctx) {
|
|
258
|
+
const commentId = ctx.getPositionalArg(2);
|
|
259
|
+
if (!commentId) {
|
|
260
|
+
throw new Error(reportCommentsUsage('reply'));
|
|
261
|
+
}
|
|
262
|
+
const response = await ctx.authedFetch(`/api/cli/report-comments/${encodeURIComponent(commentId)}`, {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
headers: { 'Content-Type': 'application/json' },
|
|
265
|
+
body: JSON.stringify({
|
|
266
|
+
body: readReportCommentBodyInput(ctx, 'reply'),
|
|
267
|
+
via: readReportCommentViaInput(ctx),
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
throw new Error(`Failed to reply to report comment: ${await readReportCommentError(ctx, response)}`);
|
|
272
|
+
}
|
|
273
|
+
const data = await ctx.parseJsonResponse(response, 'Report comment reply');
|
|
274
|
+
if (ctx.hasJsonFlag()) {
|
|
275
|
+
ctx.printJson(data);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
ctx.printLine(`Added reply ${ctx.sanitizeTerminalText(data.comment.id)} to ${ctx.sanitizeTerminalText(commentId)}`);
|
|
279
|
+
}
|
|
280
|
+
async function updateReportComment(ctx, action) {
|
|
281
|
+
const commentId = ctx.getPositionalArg(2);
|
|
282
|
+
if (!commentId) {
|
|
283
|
+
throw new Error(reportCommentsUsage(action));
|
|
284
|
+
}
|
|
285
|
+
const payload = action === 'edit'
|
|
286
|
+
? { body: readReportCommentBodyInput(ctx, 'edit') }
|
|
287
|
+
: { resolved: action === 'resolve' };
|
|
288
|
+
const response = await ctx.authedFetch(`/api/cli/report-comments/${encodeURIComponent(commentId)}`, {
|
|
289
|
+
method: 'PATCH',
|
|
290
|
+
headers: { 'Content-Type': 'application/json' },
|
|
291
|
+
body: JSON.stringify(payload),
|
|
292
|
+
});
|
|
293
|
+
if (!response.ok) {
|
|
294
|
+
throw new Error(`Failed to ${action} report comment: ${await readReportCommentError(ctx, response)}`);
|
|
295
|
+
}
|
|
296
|
+
const data = await ctx.parseJsonResponse(response, `Report comment ${action}`);
|
|
297
|
+
if (ctx.hasJsonFlag()) {
|
|
298
|
+
ctx.printJson(data);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const verb = action === 'unresolve' ? 'Reopened' : action === 'edit' ? 'Edited' : 'Resolved';
|
|
302
|
+
ctx.printLine(`${verb} comment ${ctx.sanitizeTerminalText(data.comment.id)}`);
|
|
303
|
+
}
|
|
304
|
+
export async function reportCommentsCommand(action, ctx) {
|
|
305
|
+
if (action === 'list') {
|
|
306
|
+
await listReportComments(ctx);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (action === 'add') {
|
|
310
|
+
await addReportComment(ctx);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (action === 'reply') {
|
|
314
|
+
await replyReportComment(ctx);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (action === 'resolve' || action === 'unresolve' || action === 'edit') {
|
|
318
|
+
await updateReportComment(ctx, action);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
throw new Error('Usage: orizu comments <list|add|reply|resolve|unresolve|edit> ...');
|
|
322
|
+
}
|
|
323
|
+
export function throwDeprecatedPromptCommentsCommand() {
|
|
324
|
+
throw new Error('orizu prompts comments has moved. Use `orizu comments list --prompt <prompt-id-or-name> --project <team/project> [--label <label> | --version <id>]`.');
|
|
325
|
+
}
|
package/dist/skill-installer.js
CHANGED
|
@@ -14,14 +14,17 @@ export const SKILL_INSTALL_TARGETS = [
|
|
|
14
14
|
'codex-project',
|
|
15
15
|
'claude-user',
|
|
16
16
|
'claude-project',
|
|
17
|
+
'opencode-user',
|
|
18
|
+
'opencode-project',
|
|
17
19
|
'agents-md',
|
|
18
20
|
];
|
|
19
21
|
export const SKILL_META_FILENAME = '.orizu-skill-meta.json';
|
|
20
|
-
export const SKILL_INSTALL_AGENTS = ['claude', 'codex'];
|
|
22
|
+
export const SKILL_INSTALL_AGENTS = ['claude', 'codex', 'opencode'];
|
|
21
23
|
const PROJECT_LEVEL_TARGETS = [
|
|
22
24
|
'agents-project',
|
|
23
25
|
'codex-project',
|
|
24
26
|
'claude-project',
|
|
27
|
+
'opencode-project',
|
|
25
28
|
];
|
|
26
29
|
function getCliRoot() {
|
|
27
30
|
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -134,12 +137,24 @@ export function getSkillInstallPath(target, options) {
|
|
|
134
137
|
if (target === 'claude-project') {
|
|
135
138
|
return resolve(cwd, '.claude', 'skills', SKILL_NAME);
|
|
136
139
|
}
|
|
140
|
+
// OpenCode discovers user skills under XDG config (~/.config/opencode/skills)
|
|
141
|
+
// and project skills under .opencode/skills (ALI-1044). The copy/link machinery
|
|
142
|
+
// is agent-neutral; only the destination path differs.
|
|
143
|
+
if (target === 'opencode-user') {
|
|
144
|
+
return resolve(home, '.config', 'opencode', 'skills', SKILL_NAME);
|
|
145
|
+
}
|
|
146
|
+
if (target === 'opencode-project') {
|
|
147
|
+
return resolve(cwd, '.opencode', 'skills', SKILL_NAME);
|
|
148
|
+
}
|
|
137
149
|
return resolve(cwd, 'AGENTS.md');
|
|
138
150
|
}
|
|
139
151
|
export function getTargetForAgent(agent, scope) {
|
|
140
152
|
if (agent === 'claude') {
|
|
141
153
|
return scope === 'global' ? 'claude-user' : 'claude-project';
|
|
142
154
|
}
|
|
155
|
+
if (agent === 'opencode') {
|
|
156
|
+
return scope === 'global' ? 'opencode-user' : 'opencode-project';
|
|
157
|
+
}
|
|
143
158
|
return scope === 'global' ? 'codex-user' : 'agents-project';
|
|
144
159
|
}
|
|
145
160
|
export function isSkillInstallAgent(value) {
|
package/package.json
CHANGED
package/src/credentials.ts
CHANGED
|
@@ -210,6 +210,52 @@ export function getActiveBaseUrl(): string | null {
|
|
|
210
210
|
return config?.activeBaseUrl || null
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* In-sandbox bearer resolution (ALI-1044). Resolves a bearer supplied out-of-band
|
|
215
|
+
* to the hosted agent, WITHOUT consulting credentials.json:
|
|
216
|
+
* 1. `ORIZU_TOKEN` — an explicit bearer in the environment (wins if present).
|
|
217
|
+
* 2. `ORIZU_TOKEN_FILE` — an absolute path to a 0600 file whose TRIMMED contents
|
|
218
|
+
* are the bearer.
|
|
219
|
+
* Returns null when neither is set, so callers fall through to credentials.json.
|
|
220
|
+
*
|
|
221
|
+
* The token file is read FRESH on EVERY call and never cached: the hosted loop
|
|
222
|
+
* ROTATES the file underneath a long-lived agent process, so caching the value
|
|
223
|
+
* would pin a stale (soon-expired) bearer. The token is never logged, echoed, or
|
|
224
|
+
* passed via argv — only returned to the caller for a single request's header.
|
|
225
|
+
*
|
|
226
|
+
* A set-but-unreadable/empty `ORIZU_TOKEN_FILE` throws a clear error rather than
|
|
227
|
+
* returning null, so a misconfigured sandbox fails loudly instead of silently
|
|
228
|
+
* falling back to (absent) credentials.json and emitting a confusing "not logged
|
|
229
|
+
* in" message.
|
|
230
|
+
*/
|
|
231
|
+
export function resolveEnvBearerToken(): string | null {
|
|
232
|
+
const explicit = process.env.ORIZU_TOKEN
|
|
233
|
+
if (explicit && explicit.trim()) {
|
|
234
|
+
return explicit.trim()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const tokenFile = process.env.ORIZU_TOKEN_FILE
|
|
238
|
+
if (tokenFile && tokenFile.length > 0) {
|
|
239
|
+
if (!existsSync(tokenFile)) {
|
|
240
|
+
throw new Error(`ORIZU_TOKEN_FILE is set to ${tokenFile} but no such file exists.`)
|
|
241
|
+
}
|
|
242
|
+
let raw: string
|
|
243
|
+
try {
|
|
244
|
+
raw = readFileSync(tokenFile, 'utf8')
|
|
245
|
+
} catch (error) {
|
|
246
|
+
const detail = isNodeError(error) ? error.message : String(error)
|
|
247
|
+
throw new Error(`Failed to read ORIZU_TOKEN_FILE (${tokenFile}): ${detail}`)
|
|
248
|
+
}
|
|
249
|
+
const token = raw.trim()
|
|
250
|
+
if (!token) {
|
|
251
|
+
throw new Error(`ORIZU_TOKEN_FILE (${tokenFile}) is empty.`)
|
|
252
|
+
}
|
|
253
|
+
return token
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
258
|
+
|
|
213
259
|
export function setActiveBaseUrl(baseUrl: string | null) {
|
|
214
260
|
const config = loadCredentialsConfigForWrite()
|
|
215
261
|
config.activeBaseUrl = baseUrl
|