convene-cli 1.8.0 → 1.9.0
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/dist/api.js +11 -0
- package/dist/commands/feedback.js +88 -0
- package/dist/index.js +9 -0
- package/dist/render.js +61 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -86,6 +86,17 @@ class ConveneApi {
|
|
|
86
86
|
const p = slug ? `/projects/${encodeURIComponent(slug)}/inbox` : '/inbox';
|
|
87
87
|
return this.request('GET', p, { timeoutMs });
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* GET /projects/:slug/feedback — the maintainer-facing feature_feedback list
|
|
91
|
+
* (read counterpart to `convene suggest`). Member-gated server-side; returns
|
|
92
|
+
* `{ items: FeedbackItem[], role }` with metadata flattened (lifecycle /
|
|
93
|
+
* category / upvotes / source_*). Read-only — the `convene feedback` command
|
|
94
|
+
* is fail-open, so callers pass a short explicit timeout.
|
|
95
|
+
*/
|
|
96
|
+
listFeedback(slug, opts = {}, timeoutMs) {
|
|
97
|
+
const qs = opts.limit ? `?limit=${opts.limit}` : '';
|
|
98
|
+
return this.request('GET', `/projects/${encodeURIComponent(slug)}/feedback${qs}`, { timeoutMs });
|
|
99
|
+
}
|
|
89
100
|
/**
|
|
90
101
|
* GET /poll — the long-poll stream `convene watch` consumes. `since` is the
|
|
91
102
|
* resume cursor (Last-Event-ID semantics, a messages.id); `wait` is the server
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterFeedback = filterFeedback;
|
|
4
|
+
exports.feedback = feedback;
|
|
5
|
+
/**
|
|
6
|
+
* `convene feedback [id]` — the READ counterpart to `convene suggest`. Lists the
|
|
7
|
+
* feature_feedback (feature requests / bugs / notes) filed for this project, or
|
|
8
|
+
* shows one item by short_id/id.
|
|
9
|
+
*
|
|
10
|
+
* FAIL-OPEN, like `convene lanes`: reading your own backlog must NEVER break a
|
|
11
|
+
* workflow. Any read failure prints a short stderr note and returns (exit 0) —
|
|
12
|
+
* it never dies/throws. (Contrast `convene inbox`, which is die-loud.)
|
|
13
|
+
*
|
|
14
|
+
* The server endpoint already exists (GET /projects/:slug/feedback, member-gated,
|
|
15
|
+
* returns `{ items, role }`); this command is CLI-only. Filters (--status, --mine,
|
|
16
|
+
* by-id) are applied CLIENT-SIDE over the returned items. The item `body` is
|
|
17
|
+
* UNTRUSTED member free-text and only ever reaches the terminal through the
|
|
18
|
+
* inertToken-sanitizing renderers in render.ts — never inlined here.
|
|
19
|
+
*/
|
|
20
|
+
const ctx_1 = require("../ctx");
|
|
21
|
+
const render_1 = require("../render");
|
|
22
|
+
const FEEDBACK_TIMEOUT_MS = 2500; // explicit short timeout — NEVER the 10s default
|
|
23
|
+
/** Apply the client-side filters in a pure, testable way. */
|
|
24
|
+
function filterFeedback(items, opts) {
|
|
25
|
+
let out = items;
|
|
26
|
+
if (opts.id) {
|
|
27
|
+
out = out.filter((f) => f.short_id === opts.id || f.id === opts.id);
|
|
28
|
+
}
|
|
29
|
+
if (opts.status) {
|
|
30
|
+
const want = opts.status.toLowerCase();
|
|
31
|
+
out = out.filter((f) => (f.lifecycle || '').toLowerCase() === want);
|
|
32
|
+
}
|
|
33
|
+
if (opts.mine && opts.member) {
|
|
34
|
+
out = out.filter((f) => f.source_member === opts.member);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
async function feedback(id, opts) {
|
|
39
|
+
try {
|
|
40
|
+
const ctx = (0, ctx_1.getContext)({ project: opts.project });
|
|
41
|
+
const slug = (0, ctx_1.requireSlug)(ctx);
|
|
42
|
+
const res = await ctx.api.listFeedback(slug, {}, FEEDBACK_TIMEOUT_MS);
|
|
43
|
+
if (!res.ok || !res.json) {
|
|
44
|
+
// Fail-open: a backlog read failure is informational, never blocking.
|
|
45
|
+
process.stderr.write(`convene: feedback UNVERIFIED — could not reach the bus (${res.error ?? res.status})\n`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const all = Array.isArray(res.json.items) ? res.json.items : [];
|
|
49
|
+
const items = filterFeedback(all, {
|
|
50
|
+
id,
|
|
51
|
+
status: opts.status,
|
|
52
|
+
mine: opts.mine,
|
|
53
|
+
member: ctx.member,
|
|
54
|
+
});
|
|
55
|
+
if (opts.json) {
|
|
56
|
+
process.stdout.write(JSON.stringify(items, null, 2) + '\n');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Single-item show.
|
|
60
|
+
if (id) {
|
|
61
|
+
if (items.length === 0) {
|
|
62
|
+
process.stdout.write(`no feedback matching "${id}" in ${slug}.\n`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const f of items)
|
|
66
|
+
process.stdout.write((0, render_1.feedbackDetail)(f) + '\n');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// List view.
|
|
70
|
+
if (items.length === 0) {
|
|
71
|
+
if (opts.status || opts.mine) {
|
|
72
|
+
process.stdout.write(`no feedback matches that filter in ${slug}.\n`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
process.stdout.write(`no feedback filed yet — file one with \`convene suggest "<text>"\`.\n`);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
process.stdout.write(`${items.length} feedback item(s) for ${slug}:\n`);
|
|
80
|
+
for (const f of items)
|
|
81
|
+
process.stdout.write((0, render_1.feedbackLine)(f) + '\n');
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
// Defensive: the command must never throw out of fail-open.
|
|
85
|
+
process.stderr.write(`convene: feedback UNVERIFIED — ${err?.message ?? 'unknown error'}\n`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,7 @@ const fetch_1 = require("./commands/fetch");
|
|
|
43
43
|
const notify_1 = require("./commands/notify");
|
|
44
44
|
const post = __importStar(require("./commands/post"));
|
|
45
45
|
const inbox_1 = require("./commands/inbox");
|
|
46
|
+
const feedback_1 = require("./commands/feedback");
|
|
46
47
|
const auth_1 = require("./commands/auth");
|
|
47
48
|
const init_1 = require("./commands/init");
|
|
48
49
|
const offboard_1 = require("./commands/offboard");
|
|
@@ -244,6 +245,14 @@ program
|
|
|
244
245
|
.option('--project <slug>')
|
|
245
246
|
.option('--json')
|
|
246
247
|
.action((opts) => (0, inbox_1.inbox)(opts));
|
|
248
|
+
program
|
|
249
|
+
.command('feedback [id]')
|
|
250
|
+
.description('list or show filed feedback (feature requests / bugs / notes) for this project')
|
|
251
|
+
.option('--status <lifecycle>', 'filter: new|triaged|planned|shipped|declined')
|
|
252
|
+
.option('--mine', 'only feedback you filed')
|
|
253
|
+
.option('--project <slug>')
|
|
254
|
+
.option('--json')
|
|
255
|
+
.action((id, opts) => (0, feedback_1.feedback)(id, opts));
|
|
247
256
|
program
|
|
248
257
|
.command('explain [question]')
|
|
249
258
|
.description('ask how Convene itself works (protocol, lanes, halts, privacy, …) — fail-soft, public')
|
package/dist/render.js
CHANGED
|
@@ -5,6 +5,8 @@ exports.inertToken = inertToken;
|
|
|
5
5
|
exports.renderHealthLine = renderHealthLine;
|
|
6
6
|
exports.renderRecentLine = renderRecentLine;
|
|
7
7
|
exports.renderOpenItem = renderOpenItem;
|
|
8
|
+
exports.feedbackLine = feedbackLine;
|
|
9
|
+
exports.feedbackDetail = feedbackDetail;
|
|
8
10
|
exports.renderChannelBlock = renderChannelBlock;
|
|
9
11
|
exports.renderSessionOpenBlock = renderSessionOpenBlock;
|
|
10
12
|
exports.renderRelocateBlock = renderRelocateBlock;
|
|
@@ -136,6 +138,65 @@ function renderOpenItem(m, member) {
|
|
|
136
138
|
// question
|
|
137
139
|
return ` [QUESTION] ${from} [id: ${m.short_id}] ${m.body ?? ''}`.trimEnd();
|
|
138
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* One terse feedback line for the list view. Consistent with laneLine/inbox rows:
|
|
143
|
+
* a `[short_id]` lead, then server-derived structured tokens (lifecycle, category,
|
|
144
|
+
* age, upvotes, source), and the UNTRUSTED `body` rendered inert + length-capped
|
|
145
|
+
* via `inertToken`. NEVER inlines the raw body.
|
|
146
|
+
*/
|
|
147
|
+
function feedbackLine(f) {
|
|
148
|
+
const age = agoLabel(f.created_at);
|
|
149
|
+
const bits = [`[${f.short_id}]`, f.lifecycle];
|
|
150
|
+
if (f.category)
|
|
151
|
+
bits.push(f.category);
|
|
152
|
+
if (age)
|
|
153
|
+
bits.push(`${age} ago`);
|
|
154
|
+
if (f.upvotes > 0)
|
|
155
|
+
bits.push(`+${f.upvotes}`);
|
|
156
|
+
const src = f.source_member
|
|
157
|
+
? `${f.source_member}${f.source_tool ? `/${f.source_tool}` : ''}`
|
|
158
|
+
: null;
|
|
159
|
+
if (src)
|
|
160
|
+
bits.push(`(${inertToken(src, 48)})`);
|
|
161
|
+
const body = inertToken(f.body);
|
|
162
|
+
return ` ${bits.join(' ')}${body ? ` — ${body}` : ''}`;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The expanded single-item view (`convene feedback <id>`). Full body (still inert)
|
|
166
|
+
* plus every flattened field, one per line. Mirrors the terse-row grammar but
|
|
167
|
+
* gives the human the complete picture for one item.
|
|
168
|
+
*/
|
|
169
|
+
function feedbackDetail(f) {
|
|
170
|
+
const L = [];
|
|
171
|
+
L.push(`[${f.short_id}] feedback`);
|
|
172
|
+
L.push(` lifecycle: ${f.lifecycle}`);
|
|
173
|
+
if (f.category)
|
|
174
|
+
L.push(` category: ${f.category}`);
|
|
175
|
+
if (f.severity)
|
|
176
|
+
L.push(` severity: ${f.severity}`);
|
|
177
|
+
if (f.tags.length)
|
|
178
|
+
L.push(` tags: ${f.tags.map((t) => inertToken(t, 32)).join(', ')}`);
|
|
179
|
+
L.push(` upvotes: ${f.upvotes}`);
|
|
180
|
+
const age = agoLabel(f.created_at);
|
|
181
|
+
L.push(` filed: ${f.created_at}${age ? ` (${age} ago)` : ''}`);
|
|
182
|
+
const src = f.source_member
|
|
183
|
+
? `${f.source_member}${f.source_tool ? ` / ${f.source_tool}` : ''}`
|
|
184
|
+
: null;
|
|
185
|
+
if (src)
|
|
186
|
+
L.push(` source: ${inertToken(src, 64)}`);
|
|
187
|
+
if (f.source_project)
|
|
188
|
+
L.push(` project: ${inertToken(f.source_project, 64)}`);
|
|
189
|
+
if (f.from_handle || f.from_session) {
|
|
190
|
+
L.push(` from: ${inertToken(f.from_session || f.from_handle || '', 64)}`);
|
|
191
|
+
}
|
|
192
|
+
if (f.bridge_of)
|
|
193
|
+
L.push(` bridge_of: ${f.bridge_of}`);
|
|
194
|
+
if (f.bridged_to)
|
|
195
|
+
L.push(` bridged_to: ${f.bridged_to}`);
|
|
196
|
+
L.push(' body (UNTRUSTED member free-text):');
|
|
197
|
+
L.push(` ${inertToken(f.body, 400)}`);
|
|
198
|
+
return L.join('\n');
|
|
199
|
+
}
|
|
139
200
|
/**
|
|
140
201
|
* One inert lane line. Only server-derived structured tokens reach the agent:
|
|
141
202
|
* the validated holder_handle, numeric ages/ETAs, the canonical lane name. The
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "convene-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Convene CLI — AI development coordination bus client + UserPromptSubmit hook. Install: npm i -g convene-cli; then `convene setup`.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://dev.convene.live",
|