@stage-labs/metro 0.1.0-beta.257 → 0.1.0-beta.258

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.257",
3
+ "version": "0.1.0-beta.258",
4
4
  "description": "The metro command line: run your agent on this machine, relaying its chat channels and connectors to Claude Code.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metro",
3
- "version": "0.1.0-beta.257",
3
+ "version": "0.1.0-beta.258",
4
4
  "description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
5
5
  "author": {
6
6
  "name": "Bonustrack",
@@ -14,17 +14,20 @@ Stage (https://stage.box) is a messenger built on XMTP. A Stage channel is an XM
14
14
  - Rename: `set_channel_metadata {"line": "…", "name": "New title"}`. No tool sets the description or the picture.
15
15
  - Add or remove people: `add_members {"line": "…", "members": ["0x…"]}`. `remove_members` takes the same shape.
16
16
  - Leave: `close_channel {"line": "…", "removeSelf": true}`.
17
- - Read: `group_info {"line": "…"}` gives the name, `labels`, `assigned`, `appData` and the members (`inboxId`, `address`). `list_members {"line": "…"}` gives the members only.
17
+ - Read: `group_info {"line": "…"}` gives the name, `labels`, `assigned`, `category`, `status`, `priority`, `appData` and the members (`inboxId`, `address`). `list_members {"line": "…"}` gives the members only.
18
18
  - Direct chat: `dm {"address": "0x…"}` opens a 1:1 chat and returns its line.
19
19
  - With several XMTP accounts, add `"account": "<id>"` to `create_group` and `dm`.
20
20
 
21
- **Labels, assignees and category.** They live in the channel's metadata, and Stage shows them on the channel.
22
- - Labels: `set_channel_metadata {"line": "…", "metadata": {"labels": ["🚧 In progress", "Metro"]}}`. At most 16 labels of 24 characters each.
21
+ **Labels, assignees, category, status and priority.** They live in the channel's metadata, and Stage shows them on the channel.
22
+ - Labels: `set_channel_metadata {"line": "…", "metadata": {"labels": ["Bug"]}}`. At most 16 labels of 24 characters each.
23
23
  - Assignees: `set_channel_metadata {"line": "…", "metadata": {"assigned": ["0x…"]}}`. Only 0x addresses of current members.
24
- - Category: `set_channel_metadata {"line": "…", "metadata": {"category": "Clients"}}`. One per channel, 24 characters at most; `""` clears it. `group_info` returns it as `category`.
25
- - Each key you send replaces that whole list. Keys you leave out stay as they are. To add one label or one assignee, read `group_info` first and send the old list plus the new one. `[]` clears a list. Read `group_info` again to check it saved.
24
+ - Category: `set_channel_metadata {"line": "…", "metadata": {"category": "Clients"}}`.
25
+ - Status: `set_channel_metadata {"line": "…", "metadata": {"status": "In review"}}`.
26
+ - Category and status are each one free-form string per channel. Both are trimmed, whitespace is collapsed, and text is truncated to 24 characters. `""` or `null` clears either field. `group_info` returns them as `category` and `status`.
27
+ - Priority: `set_channel_metadata {"line": "…", "metadata": {"priority": "High"}}`. Exactly `Urgent`, `High`, `Medium` or `Low` after trimming surrounding whitespace; empty text or `null` clears it. `group_info` returns it as `priority`.
28
+ - Each key you send replaces that whole value. Keys you leave out stay as they are. To add one label or one assignee, read `group_info` first and send the old list plus the new one. `[]` clears a list. Read `group_info` again to check it saved.
26
29
 
27
- **Task status labels.** When a channel is a task, keep exactly one of these status labels, plus any project labels:
30
+ **Task status labels.** Keep the owner's existing workflow. For a label-based task workflow, keep exactly one of these status labels, plus any project labels:
28
31
  - `🗒️ Backlog`
29
32
  - `🎯 To-do`
30
33
  - `🚧 In progress`
@@ -34,6 +37,8 @@ Stage (https://stage.box) is a messenger built on XMTP. A Stage channel is an XM
34
37
 
35
38
  Move it from Backlog or To-do to In progress, then In review, then Done. Use Blocked while you wait on someone. To change it, read `group_info`, swap the old status for the new one in `labels`, keep the other labels, and send the whole list with `set_channel_metadata` `metadata.labels`.
36
39
 
40
+ The separate `metadata.status` and `metadata.priority` fields are optional. Status is free-form, not limited to the label names above. Do not migrate or remove existing labels unless asked.
41
+
37
42
  **Messages.**
38
43
  - Send: `send {"line": "…", "text": "Hello"}`. Reply with a quote: `reply {"line": "…", "message_id": "…", "text": "…"}`.
39
44
  - React: `react {"line": "…", "message_id": "…", "emoji": "👍"}`. `unreact` takes the same shape.
@@ -167,6 +167,7 @@ const PRIOR_STAGE: ReadonlySet<string> = new Set([
167
167
  '08a30f5cd5cf8013ab70c1fa9e1109018c0337552296a8efe5c0cf543b284991',
168
168
  'f87e9494ea6151d140b742c1b88b4f435ae653559be641fddc2160cd24dd4e1b',
169
169
  'cb4307b016855c1a09d48bc8b3924a3080bd78b205963df3d6f20401d0486df9',
170
+ 'f2be51c5290d2e61b47f10c379fa45d051a59a6b3f06d99174c2a170074bc8f5',
170
171
  ]);
171
172
  const PRIOR_MEMORY: ReadonlySet<string> = new Set(['08795399635c8cdec2b50cb4e336f89ae880f3997ae52560ef647a9cdc8f1b08']);
172
173
 
@@ -107,6 +107,8 @@ async function applyChannelMeta(
107
107
  function metaFields(merged: Record<string, unknown>): {
108
108
  labels: string[];
109
109
  category: string | undefined;
110
+ status: string | undefined;
111
+ priority: string | undefined;
110
112
  github: string | undefined;
111
113
  preview: string | undefined;
112
114
  assigned: unknown;
@@ -114,6 +116,8 @@ function metaFields(merged: Record<string, unknown>): {
114
116
  return {
115
117
  labels: Array.isArray(merged.labels) ? (merged.labels as string[]) : [],
116
118
  category: typeof merged.category === 'string' ? merged.category : undefined,
119
+ status: typeof merged.status === 'string' ? merged.status : undefined,
120
+ priority: typeof merged.priority === 'string' ? merged.priority : undefined,
117
121
  github: typeof merged.github === 'string' ? merged.github : undefined,
118
122
  preview: typeof merged.preview === 'string' ? merged.preview : undefined,
119
123
  assigned: Object.hasOwn(merged, 'assigned') ? merged.assigned : [],
@@ -93,7 +93,7 @@ export async function buildGroupInfo(
93
93
  const gn = (conv as unknown as { name?: string | (() => Promise<string>) })
94
94
  .name;
95
95
  const resolvedName = typeof gn === 'function' ? await gn() : (gn ?? '');
96
- const { labels, category, github, preview } = readAppData(rawAppData);
96
+ const { labels, category, status, priority, github, preview } = readAppData(rawAppData);
97
97
  return {
98
98
  line,
99
99
  id: conv.id,
@@ -103,6 +103,8 @@ export async function buildGroupInfo(
103
103
  memberCount: inboxIds.length,
104
104
  labels,
105
105
  category,
106
+ status,
107
+ priority,
106
108
  github,
107
109
  preview,
108
110
  assigned: Object.hasOwn(appData, 'assigned') ? appData.assigned : [],
@@ -13,6 +13,8 @@ export interface GroupLike {
13
13
  const MAX_LABELS = 16;
14
14
  const MAX_LABEL_LEN = 24;
15
15
  const MAX_APP_DATA_BYTES = 8192;
16
+ export const PRIORITIES = ['Urgent', 'High', 'Medium', 'Low'] as const;
17
+ type Priority = (typeof PRIORITIES)[number];
16
18
 
17
19
  function cleanLabel(raw: string): string {
18
20
  return raw.trim().replace(/\s+/g, ' ').slice(0, MAX_LABEL_LEN);
@@ -74,13 +76,19 @@ function trimmedString(v: unknown): string | undefined {
74
76
  return typeof v === 'string' && v.trim() ? v.trim() : undefined;
75
77
  }
76
78
 
77
- function cleanCategory(v: unknown): string | undefined {
79
+ function cleanMetadataText(v: unknown): string | undefined {
78
80
  return typeof v === 'string' ? cleanLabel(v) || undefined : undefined;
79
81
  }
80
82
 
83
+ function cleanPriority(value: unknown): Priority | undefined {
84
+ return PRIORITIES.find((priority) => priority === (typeof value === 'string' ? value.trim() : value));
85
+ }
86
+
81
87
  export function readAppData(appData: string | undefined): {
82
88
  labels: string[];
83
89
  category?: string;
90
+ status?: string;
91
+ priority?: Priority;
84
92
  github?: string;
85
93
  preview?: string;
86
94
  } {
@@ -91,7 +99,9 @@ export function readAppData(appData: string | undefined): {
91
99
  const rec = p as Record<string, unknown>;
92
100
  return {
93
101
  labels: cleanLabels(rec.labels),
94
- category: cleanCategory(rec.category),
102
+ category: cleanMetadataText(rec.category),
103
+ status: cleanMetadataText(rec.status),
104
+ priority: cleanPriority(rec.priority),
95
105
  github: trimmedString(rec.github),
96
106
  preview: trimmedString(rec.preview),
97
107
  };
@@ -107,11 +117,20 @@ function checkedLabels(value: unknown): string[] {
107
117
  return cleanLabels(value);
108
118
  }
109
119
 
110
- function checkedCategory(value: unknown): string | undefined {
120
+ function checkedMetadataText(key: string, value: unknown): string | undefined {
111
121
  if (value !== undefined && value !== null && typeof value !== 'string') {
112
- throw new TrainError('INVALID_ARGS', 'category must be a string or null');
122
+ throw new TrainError('INVALID_ARGS', `${key} must be a string or null`);
123
+ }
124
+ return cleanMetadataText(value);
125
+ }
126
+
127
+ function checkedPriority(value: unknown): Priority | undefined {
128
+ if (value === undefined || value === null || (typeof value === 'string' && !value.trim())) return undefined;
129
+ const priority = cleanPriority(value);
130
+ if (!priority) {
131
+ throw new TrainError('INVALID_ARGS', 'priority must be Urgent, High, Medium, Low, or null');
113
132
  }
114
- return cleanCategory(value);
133
+ return priority;
115
134
  }
116
135
 
117
136
  const KNOWN_FIELDS = new Map<string, (value: unknown) => unknown>([
@@ -119,7 +138,9 @@ const KNOWN_FIELDS = new Map<string, (value: unknown) => unknown>([
119
138
  ['assigned', normalizeAssigned],
120
139
  ['github', (value) => normalizeGithubUrl(value) || undefined],
121
140
  ['preview', (value) => normalizePreviewUrl(value) || undefined],
122
- ['category', checkedCategory],
141
+ ['category', (value) => checkedMetadataText('category', value)],
142
+ ['status', (value) => checkedMetadataText('status', value)],
143
+ ['priority', checkedPriority],
123
144
  ]);
124
145
 
125
146
  function applyMergeKey(
@@ -1,4 +1,5 @@
1
1
  import type { StationTool } from '@metro-labs/core/stations/types';
2
+ import { PRIORITIES } from './labels.js';
2
3
  import {
3
4
  str,
4
5
  setChannelMetadata,
@@ -114,7 +115,7 @@ export const XMTP_TOOLS: StationTool[] = [
114
115
  group: 'read',
115
116
  description:
116
117
  "Read an XMTP channel's current metadata + membership. Args: line (required). Returns " +
117
- '{line, id, account, version (dm|group), name, memberCount, labels, category, github, preview, ' +
118
+ '{line, id, account, version (dm|group), name, memberCount, labels, category, status, priority, github, preview, ' +
118
119
  'assigned, appData (stored JSON object), rawAppData (stored JSON string), ' +
119
120
  'members:[{inboxId, address}]}. Syncs first and fails if metadata cannot be read. ' +
120
121
  'xmtp-only (daemon `groupInfo`). Use before ' +
@@ -172,10 +173,11 @@ export const XMTP_TOOLS: StationTool[] = [
172
173
  'any of metadata? (JSON object), github? (url), preview? (url), name? (string). ' +
173
174
  'metadata is a shallow appData patch: omitted keys are preserved, supplied nested ' +
174
175
  'values replace that key, and null deletes custom keys. v is reserved. Put labels, ' +
175
- 'assigned and category inside metadata. assigned replaces the array with normalized lowercase ' +
176
+ 'assigned, category, status and priority inside metadata. assigned replaces the array with normalized lowercase ' +
176
177
  'Ethereum addresses of current members. Read group_info and union existing IDs to ' +
177
- 'add assignees. category is one string per channel (24 characters at most); "" or null ' +
178
- 'clears it. labels remains a legacy top-level alias. Duplicate top-level/metadata ' +
178
+ 'add assignees. category and status are each one free-form string per channel, trimmed, ' +
179
+ 'whitespace collapsed and truncated to 24 characters. priority is trimmed and must be Urgent, High, Medium or Low. ' +
180
+ '"" or null clears category, status or priority. labels remains a legacy top-level alias. Duplicate top-level/metadata ' +
179
181
  'label/link aliases are refused. All fields use one updateChannelMeta call. xmtp-only. ' +
180
182
  'Returns updated metadata. Reread group_info to verify persistence.',
181
183
  inputSchema: {
@@ -193,8 +195,17 @@ export const XMTP_TOOLS: StationTool[] = [
193
195
  items: { type: 'string', pattern: '^0x[0-9a-fA-F]{40}$' },
194
196
  },
195
197
  category: {
196
- type: 'string',
197
- description: 'The one category of the channel, 24 characters at most. "" clears it.',
198
+ type: ['string', 'null'],
199
+ description: 'One free-form category, trimmed, whitespace collapsed, truncated to 24 characters. "" or null clears it.',
200
+ },
201
+ status: {
202
+ type: ['string', 'null'],
203
+ description: 'One free-form status, trimmed, whitespace collapsed, truncated to 24 characters. "" or null clears it.',
204
+ },
205
+ priority: {
206
+ type: ['string', 'null'],
207
+ enum: [...PRIORITIES, '', null],
208
+ description: 'One priority: Urgent, High, Medium or Low. Surrounding whitespace is trimmed; empty or null clears it.',
198
209
  },
199
210
  github: { type: 'string' },
200
211
  preview: { type: 'string' },
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.257"
2
+ "version": "0.1.0-beta.258"
3
3
  }