@stage-labs/metro 0.1.0-beta.248 → 0.1.0-beta.249
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 +2 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +2 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +2 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +30 -23
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +9 -4
- 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.249",
|
|
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",
|
|
@@ -18,9 +18,10 @@ Stage (https://stage.box) is a messenger built on XMTP. A Stage channel is an XM
|
|
|
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 and
|
|
21
|
+
**Labels, assignees and category.** They live in the channel's metadata, and Stage shows them on the channel.
|
|
22
22
|
- Labels: `set_channel_metadata {"line": "…", "metadata": {"labels": ["🚧 In progress", "Metro"]}}`. 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`.
|
|
24
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.
|
|
25
26
|
|
|
26
27
|
**Task status labels.** When a channel is a task, keep exactly one of these status labels, plus any project labels:
|
|
@@ -105,12 +105,14 @@ async function applyChannelMeta(
|
|
|
105
105
|
|
|
106
106
|
function metaFields(merged: Record<string, unknown>): {
|
|
107
107
|
labels: string[];
|
|
108
|
+
category: string | undefined;
|
|
108
109
|
github: string | undefined;
|
|
109
110
|
preview: string | undefined;
|
|
110
111
|
assigned: unknown;
|
|
111
112
|
} {
|
|
112
113
|
return {
|
|
113
114
|
labels: Array.isArray(merged.labels) ? (merged.labels as string[]) : [],
|
|
115
|
+
category: typeof merged.category === 'string' ? merged.category : undefined,
|
|
114
116
|
github: typeof merged.github === 'string' ? merged.github : undefined,
|
|
115
117
|
preview: typeof merged.preview === 'string' ? merged.preview : undefined,
|
|
116
118
|
assigned: Object.hasOwn(merged, 'assigned') ? merged.assigned : [],
|
|
@@ -89,7 +89,7 @@ export async function buildGroupInfo(
|
|
|
89
89
|
const gn = (conv as unknown as { name?: string | (() => Promise<string>) })
|
|
90
90
|
.name;
|
|
91
91
|
const resolvedName = typeof gn === 'function' ? await gn() : (gn ?? '');
|
|
92
|
-
const { labels, github, preview } = readAppData(rawAppData);
|
|
92
|
+
const { labels, category, github, preview } = readAppData(rawAppData);
|
|
93
93
|
return {
|
|
94
94
|
line,
|
|
95
95
|
id: conv.id,
|
|
@@ -98,6 +98,7 @@ export async function buildGroupInfo(
|
|
|
98
98
|
name: resolvedName ?? '',
|
|
99
99
|
memberCount: inboxIds.length,
|
|
100
100
|
labels,
|
|
101
|
+
category,
|
|
101
102
|
github,
|
|
102
103
|
preview,
|
|
103
104
|
assigned: Object.hasOwn(appData, 'assigned') ? appData.assigned : [],
|
|
@@ -14,13 +14,17 @@ const MAX_LABELS = 16;
|
|
|
14
14
|
const MAX_LABEL_LEN = 24;
|
|
15
15
|
const MAX_APP_DATA_BYTES = 8192;
|
|
16
16
|
|
|
17
|
+
function cleanLabel(raw: string): string {
|
|
18
|
+
return raw.trim().replace(/\s+/g, ' ').slice(0, MAX_LABEL_LEN);
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
function cleanLabels(raw: unknown): string[] {
|
|
18
22
|
if (!Array.isArray(raw)) return [];
|
|
19
23
|
const out: string[] = [];
|
|
20
24
|
const seen = new Set<string>();
|
|
21
25
|
for (const item of raw) {
|
|
22
26
|
if (typeof item !== 'string') continue;
|
|
23
|
-
const label = item
|
|
27
|
+
const label = cleanLabel(item);
|
|
24
28
|
const key = label.toLowerCase();
|
|
25
29
|
if (!label || seen.has(key)) continue;
|
|
26
30
|
seen.add(key);
|
|
@@ -70,8 +74,13 @@ function trimmedString(v: unknown): string | undefined {
|
|
|
70
74
|
return typeof v === 'string' && v.trim() ? v.trim() : undefined;
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
function cleanCategory(v: unknown): string | undefined {
|
|
78
|
+
return typeof v === 'string' ? cleanLabel(v) || undefined : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
export function readAppData(appData: string | undefined): {
|
|
74
82
|
labels: string[];
|
|
83
|
+
category?: string;
|
|
75
84
|
github?: string;
|
|
76
85
|
preview?: string;
|
|
77
86
|
} {
|
|
@@ -82,6 +91,7 @@ export function readAppData(appData: string | undefined): {
|
|
|
82
91
|
const rec = p as Record<string, unknown>;
|
|
83
92
|
return {
|
|
84
93
|
labels: cleanLabels(rec.labels),
|
|
94
|
+
category: cleanCategory(rec.category),
|
|
85
95
|
github: trimmedString(rec.github),
|
|
86
96
|
preview: trimmedString(rec.preview),
|
|
87
97
|
};
|
|
@@ -97,33 +107,30 @@ function checkedLabels(value: unknown): string[] {
|
|
|
97
107
|
return cleanLabels(value);
|
|
98
108
|
}
|
|
99
109
|
|
|
110
|
+
function checkedCategory(value: unknown): string | undefined {
|
|
111
|
+
if (value !== undefined && value !== null && typeof value !== 'string') {
|
|
112
|
+
throw new TrainError('INVALID_ARGS', 'category must be a string or null');
|
|
113
|
+
}
|
|
114
|
+
return cleanCategory(value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const KNOWN_FIELDS = new Map<string, (value: unknown) => unknown>([
|
|
118
|
+
['labels', checkedLabels],
|
|
119
|
+
['assigned', normalizeAssigned],
|
|
120
|
+
['github', (value) => normalizeGithubUrl(value) || undefined],
|
|
121
|
+
['preview', (value) => normalizePreviewUrl(value) || undefined],
|
|
122
|
+
['category', checkedCategory],
|
|
123
|
+
]);
|
|
124
|
+
|
|
100
125
|
function applyMergeKey(
|
|
101
126
|
merged: Record<string, unknown>,
|
|
102
127
|
k: string,
|
|
103
128
|
v: unknown,
|
|
104
129
|
): void {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
if (k === 'assigned') {
|
|
110
|
-
merged.assigned = normalizeAssigned(v);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (k === 'github') {
|
|
114
|
-
const g = normalizeGithubUrl(v);
|
|
115
|
-
if (g) merged.github = g;
|
|
116
|
-
else delete merged.github;
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (k === 'preview') {
|
|
120
|
-
const p = normalizePreviewUrl(v);
|
|
121
|
-
if (p) merged.preview = p;
|
|
122
|
-
else delete merged.preview;
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (v === undefined || v === null) Reflect.deleteProperty(merged, k);
|
|
126
|
-
else Object.defineProperty(merged, k, { value: v, enumerable: true, configurable: true, writable: true });
|
|
130
|
+
const normalize = KNOWN_FIELDS.get(k);
|
|
131
|
+
const value = normalize ? normalize(v) : v;
|
|
132
|
+
if (value === undefined || value === null) Reflect.deleteProperty(merged, k);
|
|
133
|
+
else Object.defineProperty(merged, k, { value, enumerable: true, configurable: true, writable: true });
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
export function mergeAppData(
|
|
@@ -114,7 +114,7 @@ export const XMTP_TOOLS: StationTool[] = [
|
|
|
114
114
|
group: 'read',
|
|
115
115
|
description:
|
|
116
116
|
"Read an XMTP channel's current metadata + membership. Args: line (required). Returns " +
|
|
117
|
-
'{line, id, account, version (dm|group), name, memberCount, labels, github, preview, ' +
|
|
117
|
+
'{line, id, account, version (dm|group), name, memberCount, labels, category, github, preview, ' +
|
|
118
118
|
'assigned, appData (stored JSON object), rawAppData (stored JSON string), ' +
|
|
119
119
|
'members:[{inboxId, address}]}. Syncs first and fails if metadata cannot be read. ' +
|
|
120
120
|
'xmtp-only (daemon `groupInfo`). Use before ' +
|
|
@@ -171,10 +171,11 @@ export const XMTP_TOOLS: StationTool[] = [
|
|
|
171
171
|
"Update an existing channel's metadata. Args: line (required, the metro:// line), and " +
|
|
172
172
|
'any of metadata? (JSON object), github? (url), preview? (url), name? (string). ' +
|
|
173
173
|
'metadata is a shallow appData patch: omitted keys are preserved, supplied nested ' +
|
|
174
|
-
'values replace that key, and null deletes custom keys. v is reserved. Put labels ' +
|
|
175
|
-
'and
|
|
174
|
+
'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
176
|
'Ethereum addresses of current members. Read group_info and union existing IDs to ' +
|
|
177
|
-
'add assignees.
|
|
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
179
|
'label/link aliases are refused. All fields use one updateChannelMeta call. xmtp-only. ' +
|
|
179
180
|
'Returns updated metadata. Reread group_info to verify persistence.',
|
|
180
181
|
inputSchema: {
|
|
@@ -191,6 +192,10 @@ export const XMTP_TOOLS: StationTool[] = [
|
|
|
191
192
|
description: 'Replacement assignee list. Current member Ethereum addresses only. [] clears.',
|
|
192
193
|
items: { type: 'string', pattern: '^0x[0-9a-fA-F]{40}$' },
|
|
193
194
|
},
|
|
195
|
+
category: {
|
|
196
|
+
type: 'string',
|
|
197
|
+
description: 'The one category of the channel, 24 characters at most. "" clears it.',
|
|
198
|
+
},
|
|
194
199
|
github: { type: 'string' },
|
|
195
200
|
preview: { type: 'string' },
|
|
196
201
|
},
|
package/runtime/runtime.json
CHANGED