@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.
@@ -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
@@ -76,6 +76,7 @@ export function loadEnv() {
76
76
  if (config.slack?.listId === 'FROM_ENV' && process.env.LIST_ID) {
77
77
  config.slack.listId = process.env.LIST_ID;
78
78
  }
79
+
79
80
  }
80
81
 
81
82
  // ─── Local lib imports (all bundled in stask/lib/) ─────────────────
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web42/stask",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "SQLite-backed task lifecycle CLI with Slack sync for AI agent teams",
5
5
  "type": "module",
6
6
  "bin": {