@integrity-labs/agt-cli 0.23.1 → 0.24.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/mcp/index.js CHANGED
@@ -22293,6 +22293,77 @@ async function registerForwardedApiTools() {
22293
22293
  }
22294
22294
  return { registered, skipped };
22295
22295
  }
22296
+ server.tool(
22297
+ "projects.list",
22298
+ "List artefacts (websites, slide decks, mockups) this agent has previously published. Call this BEFORE re-publishing to find and modify existing work instead of creating duplicates.",
22299
+ {
22300
+ publisher: external_exports.string().optional().describe('Filter by publisher (e.g. "here-now")'),
22301
+ kind: external_exports.enum(["mockup", "deck", "site", "app", "other"]).optional().describe("Filter by artefact kind"),
22302
+ limit: external_exports.number().int().min(1).max(100).optional().describe("Maximum projects to return (default 50, max 100)")
22303
+ },
22304
+ async (params) => {
22305
+ const data = await apiPost("/host/my-published-projects", {
22306
+ agent_id: AGT_AGENT_ID,
22307
+ publisher: params.publisher,
22308
+ kind: params.kind,
22309
+ limit: params.limit
22310
+ });
22311
+ if (!data.projects.length) {
22312
+ return { content: [{ type: "text", text: "No published projects yet." }] };
22313
+ }
22314
+ const lines = data.projects.map((p) => {
22315
+ const title = p.title ?? "(untitled)";
22316
+ const expires = p.expires_at ? ` expires ${p.expires_at}` : "";
22317
+ return `- [${p.kind}] ${title} \u2014 ${p.live_url} (id: ${p.id}, ${p.mode}, published ${p.published_at}${expires})`;
22318
+ });
22319
+ return { content: [{ type: "text", text: lines.join("\n") }] };
22320
+ }
22321
+ );
22322
+ server.tool(
22323
+ "projects.get",
22324
+ "Fetch metadata for a single published project. Returns the project record without the source HTML \u2014 call projects.get_source_chunk to retrieve the HTML in chunks when you need to modify it.",
22325
+ {
22326
+ project_id: external_exports.string().uuid().describe("The project id returned by projects.list")
22327
+ },
22328
+ async (params) => {
22329
+ const data = await apiPost(`/host/published-project/${encodeURIComponent(params.project_id)}`, {
22330
+ agent_id: AGT_AGENT_ID
22331
+ });
22332
+ const p = data.project;
22333
+ const header = [
22334
+ `Project: ${p.title ?? "(untitled)"} [${p.kind}]`,
22335
+ `Publisher: ${p.publisher} (${p.mode})`,
22336
+ `Live URL: ${p.live_url}`,
22337
+ p.claim_url ? `Claim URL: ${p.claim_url}` : null,
22338
+ `Published: ${p.published_at}`,
22339
+ p.expires_at ? `Expires: ${p.expires_at}` : null,
22340
+ `Last modified: ${p.last_modified_at}`,
22341
+ p.has_source_html ? `Source HTML: ${p.source_html_length} bytes (call projects.get_source_chunk to fetch)` : "(source HTML not preserved for this project)"
22342
+ ].filter((s) => s !== null).join("\n");
22343
+ return { content: [{ type: "text", text: header }] };
22344
+ }
22345
+ );
22346
+ server.tool(
22347
+ "projects.get_source_chunk",
22348
+ "Fetch a chunk of source HTML for a published project. Use this to assemble the HTML for modification when projects.get reports a non-zero source_html_length.",
22349
+ {
22350
+ project_id: external_exports.string().uuid().describe("The project id returned by projects.list"),
22351
+ offset: external_exports.number().int().min(0).describe("Byte offset into the source HTML to start from"),
22352
+ limit: external_exports.number().int().min(1).max(65536).optional().describe("Max bytes to return (default 32KB, max 64KB)")
22353
+ },
22354
+ async (params) => {
22355
+ const data = await apiPost(`/host/published-project/${encodeURIComponent(params.project_id)}/source-chunk`, {
22356
+ agent_id: AGT_AGENT_ID,
22357
+ offset: params.offset,
22358
+ limit: params.limit
22359
+ });
22360
+ const trailer = data.end_of_content ? "\n\n---\n(end of source)" : `
22361
+
22362
+ ---
22363
+ (more \u2014 next offset: ${data.offset + data.length}, total: ${data.total_length} bytes)`;
22364
+ return { content: [{ type: "text", text: data.chunk + trailer }] };
22365
+ }
22366
+ );
22296
22367
  async function main() {
22297
22368
  try {
22298
22369
  const result = await registerForwardedApiTools();
@@ -14890,21 +14890,16 @@ var StdioServerTransport = class {
14890
14890
  // src/slack-channel.ts
14891
14891
  import {
14892
14892
  chmodSync,
14893
- closeSync,
14894
14893
  createWriteStream,
14895
14894
  existsSync,
14896
- fsyncSync,
14897
- ftruncateSync,
14898
14895
  mkdirSync as mkdirSync2,
14899
- openSync,
14900
14896
  readFileSync as readFileSync2,
14901
14897
  readdirSync,
14902
14898
  renameSync,
14903
- statSync as statSync2,
14899
+ statSync,
14904
14900
  unlinkSync,
14905
14901
  watch,
14906
- writeFileSync as writeFileSync2,
14907
- writeSync
14902
+ writeFileSync as writeFileSync2
14908
14903
  } from "fs";
14909
14904
  import { basename, join as join2, resolve as resolve2 } from "path";
14910
14905
  import { homedir as homedir2 } from "os";
@@ -15503,40 +15498,6 @@ function createSlackBotUserIdClient(args) {
15503
15498
  };
15504
15499
  }
15505
15500
 
15506
- // src/pending-reminder-schedule.ts
15507
- import { statSync } from "fs";
15508
- var REMINDER_SCHEDULE_MS = [5, 10, 20].map((m) => m * 6e4);
15509
- var MAX_REMINDERS = REMINDER_SCHEDULE_MS.length;
15510
- var RESPONSE_TIMEOUT_MS = REMINDER_SCHEDULE_MS[0];
15511
- function nextReminderFireAtMs(receivedAtMs, currentReminderCount) {
15512
- const safeCount = Number.isFinite(currentReminderCount) ? Math.max(0, Math.trunc(currentReminderCount)) : 0;
15513
- let elapsed = 0;
15514
- for (let i = 0; i <= safeCount && i < MAX_REMINDERS; i++) {
15515
- elapsed += REMINDER_SCHEDULE_MS[i];
15516
- }
15517
- return receivedAtMs + elapsed;
15518
- }
15519
- function reminderText(reminderIndex, cancelCommandAvailable) {
15520
- switch (reminderIndex) {
15521
- case 0:
15522
- return "Heads-up \u2014 I'm still on it. This one's taking more than the usual minute.";
15523
- case 1:
15524
- return "Still working. Going deeper than expected; back when I have something useful.";
15525
- case 2:
15526
- default:
15527
- return cancelCommandAvailable ? "I'm 30+ min in on this. Type `/cancel` to abort, or ping me if it's urgent and I'll re-scope." : "I'm 30+ min in on this. If the answer's urgent, ping me and I'll pause to triage. Otherwise I'll keep going.";
15528
- }
15529
- }
15530
- function agentTurnEndedSinceInbound(lastStopAtPath, receivedAtMs, deps = { statSync }) {
15531
- if (!lastStopAtPath) return false;
15532
- try {
15533
- const stat = deps.statSync(lastStopAtPath);
15534
- return stat.mtimeMs > receivedAtMs;
15535
- } catch {
15536
- return false;
15537
- }
15538
- }
15539
-
15540
15501
  // src/slack-channel.ts
15541
15502
  var BOT_TOKEN = process.env.SLACK_BOT_TOKEN;
15542
15503
  var APP_TOKEN = process.env.SLACK_APP_TOKEN;
@@ -15581,12 +15542,9 @@ var SLACK_PEER_CLASSIFIER_CONFIG = {
15581
15542
  peers: parsePeersEnv(process.env.SLACK_PEERS, process.env.SLACK_PEERS_GATE),
15582
15543
  peer_disabled_mode: SLACK_PEER_DISABLED_MODE
15583
15544
  };
15584
- var pendingMessages = /* @__PURE__ */ new Map();
15585
- var CANCEL_COMMAND_AVAILABLE = false;
15586
15545
  var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join2(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
15587
15546
  var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join2(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
15588
15547
  var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join2(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
15589
- var LAST_STOP_AT_PATH = SLACK_AGENT_DIR ? join2(SLACK_AGENT_DIR, "last-stop-at") : null;
15590
15548
  var SLACK_MAX_RECOVERY_ATTEMPTS = 3;
15591
15549
  function redactSlackId(id) {
15592
15550
  if (!id) return "<none>";
@@ -15619,57 +15577,6 @@ function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
15619
15577
  );
15620
15578
  }
15621
15579
  }
15622
- function updateSlackPendingInboundMarker(marker) {
15623
- const path = slackPendingInboundPath(marker.channel, marker.thread_ts, marker.message_ts);
15624
- if (!path) return false;
15625
- let fd;
15626
- try {
15627
- fd = openSync(path, "r+");
15628
- } catch (err) {
15629
- const code = err.code;
15630
- if (code === "ENOENT") return false;
15631
- process.stderr.write(
15632
- `slack-channel(${AGENT_CODE_NAME}): pending-inbound marker open-for-update failed: ${err.message}
15633
- `
15634
- );
15635
- return false;
15636
- }
15637
- try {
15638
- const payload = Buffer.from(JSON.stringify(marker));
15639
- ftruncateSync(fd, 0);
15640
- writeSync(fd, payload, 0, payload.length, 0);
15641
- fsyncSync(fd);
15642
- return true;
15643
- } catch (err) {
15644
- process.stderr.write(
15645
- `slack-channel(${AGENT_CODE_NAME}): pending-inbound marker write failed: ${err.message}
15646
- `
15647
- );
15648
- return false;
15649
- } finally {
15650
- try {
15651
- closeSync(fd);
15652
- } catch {
15653
- }
15654
- }
15655
- }
15656
- function readSlackPendingInboundMarker(channel, threadTs, messageTs) {
15657
- const path = slackPendingInboundPath(channel, threadTs, messageTs);
15658
- if (!path || !existsSync(path)) return null;
15659
- try {
15660
- return JSON.parse(readFileSync2(path, "utf-8"));
15661
- } catch {
15662
- return null;
15663
- }
15664
- }
15665
- function clearSlackPendingInboundMarker(channel, threadTs, messageTs) {
15666
- const path = slackPendingInboundPath(channel, threadTs, messageTs);
15667
- if (!path) return;
15668
- try {
15669
- if (existsSync(path)) unlinkSync(path);
15670
- } catch {
15671
- }
15672
- }
15673
15580
  function clearAllSlackPendingMarkersForThread(channel, threadTs) {
15674
15581
  if (!SLACK_PENDING_INBOUND_DIR) return;
15675
15582
  const safeChan = channel.replace(/[^A-Za-z0-9_-]/g, "_");
@@ -15816,7 +15723,7 @@ function scanSlackRecoveryRetries() {
15816
15723
  if (!f.includes(".retry-") || f.endsWith(".poison.json")) continue;
15817
15724
  let mtimeMs;
15818
15725
  try {
15819
- mtimeMs = statSync2(join2(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
15726
+ mtimeMs = statSync(join2(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
15820
15727
  } catch {
15821
15728
  continue;
15822
15729
  }
@@ -15861,105 +15768,11 @@ function startSlackRecoveryOutboxWatcher() {
15861
15768
  retryTimer.unref?.();
15862
15769
  }
15863
15770
  startSlackRecoveryOutboxWatcher();
15864
- var SLACK_REMINDER_FETCH_TIMEOUT_MS = 1e4;
15865
- async function slackFetchWithTimeout(url, body) {
15866
- const controller = new AbortController();
15867
- const timeoutId = setTimeout(() => controller.abort(), SLACK_REMINDER_FETCH_TIMEOUT_MS);
15868
- try {
15869
- await fetch(url, {
15870
- method: "POST",
15871
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${BOT_TOKEN}` },
15872
- body: JSON.stringify(body),
15873
- signal: controller.signal
15874
- });
15875
- } finally {
15876
- clearTimeout(timeoutId);
15877
- }
15878
- }
15879
- async function fireSlackResponseTimeout(channel, threadTs, messageTs) {
15880
- const marker = readSlackPendingInboundMarker(channel, threadTs, messageTs);
15881
- if (!marker) {
15882
- return;
15883
- }
15884
- const receivedAtMs = Date.parse(marker.received_at);
15885
- if (Number.isFinite(receivedAtMs) && agentTurnEndedSinceInbound(LAST_STOP_AT_PATH, receivedAtMs)) {
15886
- process.stderr.write(
15887
- `slack-channel(${AGENT_CODE_NAME}): suppressing reminder for ${redactSlackId(messageTs)} in ${redactSlackId(channel)} \u2014 turn ended after inbound at ${marker.received_at}; clearing marker
15888
- `
15889
- );
15890
- const key2 = `${channel}:${threadTs}:${messageTs}`;
15891
- const existing = pendingMessages.get(key2);
15892
- if (existing) {
15893
- clearTimeout(existing);
15894
- pendingMessages.delete(key2);
15895
- }
15896
- clearSlackPendingInboundMarker(channel, threadTs, messageTs);
15897
- return;
15898
- }
15899
- const reminderIndex = marker.reminder_count ?? 0;
15900
- if (reminderIndex >= MAX_REMINDERS) {
15901
- return;
15902
- }
15903
- try {
15904
- await slackFetchWithTimeout("https://slack.com/api/chat.postMessage", {
15905
- channel,
15906
- thread_ts: threadTs,
15907
- text: reminderText(reminderIndex, CANCEL_COMMAND_AVAILABLE)
15908
- });
15909
- } catch {
15910
- }
15911
- if (reminderIndex === 1) {
15912
- try {
15913
- await slackFetchWithTimeout("https://slack.com/api/reactions.remove", {
15914
- channel,
15915
- timestamp: messageTs,
15916
- name: "eyes"
15917
- });
15918
- } catch {
15919
- }
15920
- try {
15921
- await slackFetchWithTimeout("https://slack.com/api/reactions.add", {
15922
- channel,
15923
- timestamp: messageTs,
15924
- name: "hourglass_flowing_sand"
15925
- });
15926
- } catch {
15927
- }
15928
- }
15929
- const nextCount = reminderIndex + 1;
15930
- const persisted = updateSlackPendingInboundMarker({ ...marker, reminder_count: nextCount });
15931
- if (!persisted) {
15932
- process.stderr.write(
15933
- `slack-channel(${AGENT_CODE_NAME}): reminder ${nextCount}/${MAX_REMINDERS} posted but marker cleared mid-fire (agent replied) \u2014 not arming next timer
15934
- `
15935
- );
15936
- return;
15937
- }
15938
- process.stderr.write(
15939
- `slack-channel(${AGENT_CODE_NAME}): reminder ${nextCount}/${MAX_REMINDERS} posted for message ${redactSlackId(messageTs)} in ${redactSlackId(channel)} (marker left pending)
15940
- `
15941
- );
15942
- if (nextCount < MAX_REMINDERS) {
15943
- armSlackPendingTimer(channel, threadTs, messageTs, REMINDER_SCHEDULE_MS[nextCount]);
15944
- }
15945
- }
15946
- function armSlackPendingTimer(channel, threadTs, messageTs, durationMs) {
15947
- const key2 = `${channel}:${threadTs}:${messageTs}`;
15948
- const existing = pendingMessages.get(key2);
15949
- if (existing) clearTimeout(existing);
15950
- const timer = setTimeout(() => {
15951
- pendingMessages.delete(key2);
15952
- void fireSlackResponseTimeout(channel, threadTs, messageTs);
15953
- }, Math.max(0, durationMs));
15954
- timer.unref?.();
15955
- pendingMessages.set(key2, timer);
15956
- }
15771
+ var STALE_MARKER_MS = 24 * 60 * 60 * 1e3;
15957
15772
  function trackPendingMessage(channel, threadTs, messageTs) {
15958
15773
  writeSlackPendingInboundMarker(channel, threadTs, messageTs);
15959
- armSlackPendingTimer(channel, threadTs, messageTs, RESPONSE_TIMEOUT_MS);
15960
15774
  }
15961
- var STALE_MARKER_MS = 24 * 60 * 60 * 1e3;
15962
- function rearmSlackPendingTimersFromDisk() {
15775
+ function sweepSlackStaleMarkersOnBoot() {
15963
15776
  if (!SLACK_PENDING_INBOUND_DIR) return;
15964
15777
  if (!existsSync(SLACK_PENDING_INBOUND_DIR)) return;
15965
15778
  let filenames;
@@ -15967,16 +15780,13 @@ function rearmSlackPendingTimersFromDisk() {
15967
15780
  filenames = readdirSync(SLACK_PENDING_INBOUND_DIR);
15968
15781
  } catch (err) {
15969
15782
  process.stderr.write(
15970
- `slack-channel(${AGENT_CODE_NAME}): rearm readdir failed: ${err.message}
15783
+ `slack-channel(${AGENT_CODE_NAME}): stale-marker readdir failed: ${err.message}
15971
15784
  `
15972
15785
  );
15973
15786
  return;
15974
15787
  }
15975
15788
  const now = Date.now();
15976
- let armed = 0;
15977
- let firedNow = 0;
15978
15789
  let cleared = 0;
15979
- let exhausted = 0;
15980
15790
  for (const filename of filenames) {
15981
15791
  if (!filename.endsWith(".json")) continue;
15982
15792
  if (filename.endsWith(".tmp")) continue;
@@ -15986,25 +15796,18 @@ function rearmSlackPendingTimersFromDisk() {
15986
15796
  marker = JSON.parse(readFileSync2(fullPath, "utf-8"));
15987
15797
  } catch (err) {
15988
15798
  process.stderr.write(
15989
- `slack-channel(${AGENT_CODE_NAME}): rearm parse failed for ${filename}: ${err.message}
15799
+ `slack-channel(${AGENT_CODE_NAME}): stale-marker parse failed for ${redactSlackId(filename)}: ${err.message}
15990
15800
  `
15991
15801
  );
15992
15802
  try {
15993
15803
  unlinkSync(fullPath);
15994
15804
  } catch {
15995
15805
  }
15806
+ cleared++;
15996
15807
  continue;
15997
15808
  }
15998
15809
  const { channel, thread_ts, message_ts, received_at } = marker;
15999
15810
  if (!channel || !thread_ts || !message_ts || !received_at) {
16000
- try {
16001
- unlinkSync(fullPath);
16002
- } catch {
16003
- }
16004
- continue;
16005
- }
16006
- const receivedAtMs = Date.parse(received_at);
16007
- if (!Number.isFinite(receivedAtMs)) {
16008
15811
  try {
16009
15812
  unlinkSync(fullPath);
16010
15813
  } catch {
@@ -16012,49 +15815,24 @@ function rearmSlackPendingTimersFromDisk() {
16012
15815
  cleared++;
16013
15816
  continue;
16014
15817
  }
16015
- if (now - receivedAtMs > STALE_MARKER_MS) {
16016
- process.stderr.write(
16017
- `slack-channel(${AGENT_CODE_NAME}): rearm skipping stale marker channel=${redactSlackId(channel)} message=${redactSlackId(message_ts)}
16018
- `
16019
- );
15818
+ const receivedAtMs = Date.parse(received_at);
15819
+ if (!Number.isFinite(receivedAtMs) || receivedAtMs > now || now - receivedAtMs > STALE_MARKER_MS) {
16020
15820
  try {
16021
15821
  unlinkSync(fullPath);
16022
15822
  } catch {
16023
15823
  }
16024
15824
  cleared++;
16025
- continue;
16026
- }
16027
- const reminderCount = marker.reminder_count ?? 0;
16028
- if (reminderCount >= MAX_REMINDERS) {
16029
- exhausted++;
16030
- continue;
16031
- }
16032
- const nextFireAt = nextReminderFireAtMs(receivedAtMs, reminderCount);
16033
- const remainingMs = nextFireAt - now;
16034
- if (remainingMs <= 0) {
16035
- pendingMessages.delete(`${channel}:${thread_ts}:${message_ts}`);
16036
- void fireSlackResponseTimeout(channel, thread_ts, message_ts);
16037
- firedNow++;
16038
- } else {
16039
- armSlackPendingTimer(channel, thread_ts, message_ts, remainingMs);
16040
- armed++;
16041
15825
  }
16042
15826
  }
16043
- if (armed > 0 || firedNow > 0 || cleared > 0 || exhausted > 0) {
15827
+ if (cleared > 0) {
16044
15828
  process.stderr.write(
16045
- `slack-channel(${AGENT_CODE_NAME}): rearm summary armed=${armed} fired_now=${firedNow} stale_cleared=${cleared} reminders_exhausted=${exhausted}
15829
+ `slack-channel(${AGENT_CODE_NAME}): stale-marker sweep cleared=${cleared}
16046
15830
  `
16047
15831
  );
16048
15832
  }
16049
15833
  }
16050
- rearmSlackPendingTimersFromDisk();
15834
+ sweepSlackStaleMarkersOnBoot();
16051
15835
  function clearPendingMessage(channel, threadTs) {
16052
- for (const [key2, timer] of pendingMessages) {
16053
- if (key2.startsWith(`${channel}:${threadTs}:`)) {
16054
- clearTimeout(timer);
16055
- pendingMessages.delete(key2);
16056
- }
16057
- }
16058
15836
  clearAllSlackPendingMarkersForThread(channel, threadTs);
16059
15837
  }
16060
15838
  function noteThreadActivity(channel, threadTs) {
@@ -16064,16 +15842,24 @@ function noteThreadActivity(channel, threadTs) {
16064
15842
  function noteThreadActivityByMessageTs(channel, messageTs) {
16065
15843
  if (!channel || !messageTs) return;
16066
15844
  clearPendingMessage(channel, messageTs);
16067
- const suffix = `:${messageTs}`;
16068
- for (const [key2, timer] of pendingMessages) {
16069
- if (!key2.startsWith(`${channel}:`)) continue;
16070
- if (!key2.endsWith(suffix)) continue;
16071
- clearTimeout(timer);
16072
- pendingMessages.delete(key2);
16073
- const parts = key2.split(":");
16074
- if (parts.length >= 3) {
16075
- const threadTs = parts[1];
16076
- clearSlackPendingInboundMarker(channel, threadTs, messageTs);
15845
+ if (!SLACK_PENDING_INBOUND_DIR) return;
15846
+ if (!existsSync(SLACK_PENDING_INBOUND_DIR)) return;
15847
+ let filenames;
15848
+ try {
15849
+ filenames = readdirSync(SLACK_PENDING_INBOUND_DIR);
15850
+ } catch {
15851
+ return;
15852
+ }
15853
+ const safeChannel = channel.replace(/[^A-Za-z0-9_-]/g, "_");
15854
+ const safeMessageTs = messageTs.replace(/[^A-Za-z0-9_-]/g, "_");
15855
+ const channelPrefix = `${safeChannel}__`;
15856
+ const messageSuffix = `__${safeMessageTs}.json`;
15857
+ for (const filename of filenames) {
15858
+ if (!filename.startsWith(channelPrefix)) continue;
15859
+ if (!filename.endsWith(messageSuffix)) continue;
15860
+ try {
15861
+ unlinkSync(join2(SLACK_PENDING_INBOUND_DIR, filename));
15862
+ } catch {
16077
15863
  }
16078
15864
  }
16079
15865
  }
@@ -17028,7 +16814,7 @@ ${formatted}` : "Thread is empty or not found."
17028
16814
  let bytes;
17029
16815
  let size;
17030
16816
  try {
17031
- const stat = statSync2(resolvedPath);
16817
+ const stat = statSync(resolvedPath);
17032
16818
  if (!stat.isFile()) {
17033
16819
  return {
17034
16820
  content: [{ type: "text", text: `Upload refused: ${resolvedPath} is not a regular file.` }],