@stage-labs/metro 0.1.0-beta.256 → 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 +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/marketplace/plugin/STAGE.md +11 -6
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +1 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/fallback.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +2 -2
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +4 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +3 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +27 -6
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +17 -6
- package/runtime/runtime.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
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
|
|
22
|
-
- Labels: `set_channel_metadata {"line": "…", "metadata": {"labels": ["
|
|
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"}}`.
|
|
25
|
-
-
|
|
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.**
|
|
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
|
|
|
@@ -29,7 +29,7 @@ export function forgetFallbackState(): void {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function scopeOf(label: string): string | null {
|
|
32
|
-
if (!label.startsWith(SCOPED)) return null;
|
|
32
|
+
if (!label.startsWith(SCOPED) || label === 'Weekly, all models') return null;
|
|
33
33
|
return (label.slice(SCOPED.length).trim().split(/\s+/)[0] ?? '').toLowerCase();
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -181,7 +181,7 @@ function whenFrom(raw: string | null, now: Date, relative = false): string | nul
|
|
|
181
181
|
|
|
182
182
|
const ANTHROPIC_WINDOWS: [string, string][] = [
|
|
183
183
|
['5h', '5-hour window'],
|
|
184
|
-
['7d', 'Weekly'],
|
|
184
|
+
['7d', 'Weekly, all models'],
|
|
185
185
|
['7d_sonnet', 'Weekly, Sonnet'],
|
|
186
186
|
];
|
|
187
187
|
|
|
@@ -273,7 +273,7 @@ const numberOf = (raw: unknown): number | null => (typeof raw === 'number' && Nu
|
|
|
273
273
|
|
|
274
274
|
const CLAUDE_WINDOWS: [string, string][] = [
|
|
275
275
|
['five_hour', '5-hour window'],
|
|
276
|
-
['seven_day', 'Weekly'],
|
|
276
|
+
['seven_day', 'Weekly, all models'],
|
|
277
277
|
['seven_day_sonnet', 'Weekly, Sonnet'],
|
|
278
278
|
['seven_day_opus', 'Weekly, Opus'],
|
|
279
279
|
];
|
|
@@ -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
|
|
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:
|
|
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
|
|
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',
|
|
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
|
|
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',
|
|
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
|
|
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
|
|
178
|
-
'
|
|
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: '
|
|
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' },
|
package/runtime/runtime.json
CHANGED