@web42/stask 0.1.5 → 0.1.6
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/commands/create.mjs +50 -1
- package/lib/env.mjs +1 -0
- package/lib/slack-api.mjs +20 -0
- package/lib/slack-row.mjs +16 -0
- package/package.json +1 -1
package/commands/create.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import fs from 'fs';
|
|
|
9
9
|
import { createHash } from 'crypto';
|
|
10
10
|
import { CONFIG, getWorkspaceLibs } from '../lib/env.mjs';
|
|
11
11
|
import { withTransaction } from '../lib/tx.mjs';
|
|
12
|
-
import { syncTaskToSlack } from '../lib/slack-row.mjs';
|
|
12
|
+
import { syncTaskToSlack, setThreadRef } from '../lib/slack-row.mjs';
|
|
13
13
|
|
|
14
14
|
function parseArgs(argv) {
|
|
15
15
|
const args = {};
|
|
@@ -21,6 +21,31 @@ function parseArgs(argv) {
|
|
|
21
21
|
return args;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Discover the list item's comment thread on the list channel.
|
|
26
|
+
* Slack Lists internally use a channel (list ID with F→C prefix swap).
|
|
27
|
+
* Each list item gets a thread whose ts shares the same epoch second
|
|
28
|
+
* as the item's date_created.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} slackApi - Slack API module
|
|
31
|
+
* @param {string} listChannelId - List channel ID (C-prefixed)
|
|
32
|
+
* @param {number} itemDateCreated - Item's date_created (Unix epoch)
|
|
33
|
+
*/
|
|
34
|
+
async function discoverListItemThread(slackApi, listChannelId, itemDateCreated) {
|
|
35
|
+
const epoch = String(itemDateCreated);
|
|
36
|
+
const oldest = String(itemDateCreated - 2);
|
|
37
|
+
const latest = String(itemDateCreated + 5);
|
|
38
|
+
|
|
39
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
40
|
+
const result = await slackApi.getChannelHistory(listChannelId, { oldest, latest, limit: 10 });
|
|
41
|
+
const messages = result.messages || [];
|
|
42
|
+
const match = messages.find(m => m.ts && m.ts.startsWith(epoch + '.'));
|
|
43
|
+
if (match) return match.ts;
|
|
44
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
export async function run(argv) {
|
|
25
50
|
const args = parseArgs(argv);
|
|
26
51
|
|
|
@@ -84,5 +109,29 @@ export async function run(argv) {
|
|
|
84
109
|
}
|
|
85
110
|
);
|
|
86
111
|
|
|
112
|
+
// Post-commit: discover list item thread and post creation message (best-effort)
|
|
113
|
+
try {
|
|
114
|
+
const listChannelId = CONFIG.slack.listId.replace(/^F/, 'C');
|
|
115
|
+
const db = libs.trackerDb.getDb();
|
|
116
|
+
// Get the Slack item's date_created (Unix epoch) to match the thread ts
|
|
117
|
+
const { getSlackRowId } = await import('../lib/slack-row.mjs');
|
|
118
|
+
const rowId = getSlackRowId(db, result.taskId);
|
|
119
|
+
const itemInfo = await libs.slackApi.slackApiRequest('POST', '/slackLists.items.info', {
|
|
120
|
+
list_id: CONFIG.slack.listId, id: rowId,
|
|
121
|
+
});
|
|
122
|
+
const dateCreated = itemInfo.record?.date_created;
|
|
123
|
+
if (dateCreated) {
|
|
124
|
+
const threadTs = await discoverListItemThread(libs.slackApi, listChannelId, dateCreated);
|
|
125
|
+
if (threadTs) {
|
|
126
|
+
const humanMention = `<@${CONFIG.human.slackUserId}>`;
|
|
127
|
+
const msg = `Creating this thread to discuss *${result.taskId}: ${args.name}*. This will be the thread where we post updates and talk about this task.\n\n${humanMention} spec is ready for your review. Let me know what you think!`;
|
|
128
|
+
await libs.slackApi.postMessage(listChannelId, msg, { threadTs });
|
|
129
|
+
setThreadRef(db, result.taskId, listChannelId, threadTs);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error(`WARNING: Thread linking failed: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
87
136
|
console.log(`Created ${result.taskId}: "${args.name}" | Status: To-Do | Assigned: ${CONFIG.human.name} | Spec: ${fileId}`);
|
|
88
137
|
}
|
package/lib/env.mjs
CHANGED
package/lib/slack-api.mjs
CHANGED
|
@@ -187,6 +187,26 @@ export async function updateListCells(listId, cells) {
|
|
|
187
187
|
});
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Fetch recent messages from a channel (conversations.history).
|
|
192
|
+
*/
|
|
193
|
+
export async function getChannelHistory(channelId, { limit = 20, oldest, latest } = {}) {
|
|
194
|
+
const payload = { channel: channelId, limit };
|
|
195
|
+
if (oldest) payload.oldest = oldest;
|
|
196
|
+
if (latest) payload.latest = latest;
|
|
197
|
+
return slackApiRequest('POST', '/conversations.history', payload);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Post a message to a channel (optionally as a thread reply).
|
|
202
|
+
* Returns the full Slack response including ts (message timestamp).
|
|
203
|
+
*/
|
|
204
|
+
export async function postMessage(channelId, text, { threadTs, unfurlLinks } = {}) {
|
|
205
|
+
const payload = { channel: channelId, text, unfurl_links: unfurlLinks ?? false };
|
|
206
|
+
if (threadTs) payload.thread_ts = threadTs;
|
|
207
|
+
return slackApiRequest('POST', '/chat.postMessage', payload);
|
|
208
|
+
}
|
|
209
|
+
|
|
190
210
|
/**
|
|
191
211
|
* Delete a row from a Slack List.
|
|
192
212
|
*/
|
package/lib/slack-row.mjs
CHANGED
|
@@ -26,6 +26,9 @@ let _rowTableCreated = false;
|
|
|
26
26
|
function ensureRowIdsTable(db) {
|
|
27
27
|
if (_rowTableCreated) return;
|
|
28
28
|
db.exec(ROW_IDS_TABLE_SQL);
|
|
29
|
+
// Migrate: add thread tracking columns (no-op if already present)
|
|
30
|
+
try { db.exec('ALTER TABLE slack_row_ids ADD COLUMN channel_id TEXT'); } catch {}
|
|
31
|
+
try { db.exec('ALTER TABLE slack_row_ids ADD COLUMN thread_ts TEXT'); } catch {}
|
|
29
32
|
_rowTableCreated = true;
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -45,6 +48,19 @@ function setSlackRowId(db, taskId, rowId) {
|
|
|
45
48
|
`).run(taskId, rowId);
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
export function getThreadRef(db, taskId) {
|
|
52
|
+
ensureRowIdsTable(db);
|
|
53
|
+
const row = db.prepare('SELECT channel_id, thread_ts FROM slack_row_ids WHERE task_id = ?').get(taskId);
|
|
54
|
+
if (!row?.channel_id || !row?.thread_ts) return null;
|
|
55
|
+
return { channelId: row.channel_id, threadTs: row.thread_ts };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function setThreadRef(db, taskId, channelId, threadTs) {
|
|
59
|
+
ensureRowIdsTable(db);
|
|
60
|
+
db.prepare('UPDATE slack_row_ids SET channel_id = ?, thread_ts = ? WHERE task_id = ?')
|
|
61
|
+
.run(channelId, threadTs, taskId);
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
// ─── Cell formatting (config-driven) ───────────────────────────────
|
|
49
65
|
|
|
50
66
|
function formatCell(fieldName, value, columnId, taskRow) {
|