@myvillage/cli 2.8.1 → 2.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myvillage/cli",
3
- "version": "2.8.1",
3
+ "version": "2.9.0",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,6 +22,14 @@ export const DATASET_APPEND_DESCRIPTION =
22
22
  'regression provide a numeric `target`. Data stays local; provenance ' +
23
23
  'is recorded automatically.';
24
24
 
25
+ export const DATASET_READ_DESCRIPTION =
26
+ 'Read a LOCAL dataset on this machine: its description, columns, labels, ' +
27
+ 'row counts, and a page of its rows (newest working set by default, or a ' +
28
+ 'frozen version). Filter by split or label and page with offset/limit. ' +
29
+ 'Table rows come back keyed by column name; image and sound rows come back ' +
30
+ 'as absolute file paths (which model_predict accepts). Read-only — use ' +
31
+ 'dataset_append to add rows.';
32
+
25
33
  export const MODEL_PREDICT_DESCRIPTION =
26
34
  'Run a prediction with a LOCAL trained model on this machine. `input` is ' +
27
35
  'text (words models), the values of one table row as a list in column ' +
@@ -38,6 +46,12 @@ export const BUILTIN_TOOL_DEFS = [
38
46
  tier: 'villager',
39
47
  builtin: true,
40
48
  },
49
+ {
50
+ name: 'dataset_read',
51
+ description: DATASET_READ_DESCRIPTION,
52
+ tier: 'villager',
53
+ builtin: true,
54
+ },
41
55
  {
42
56
  name: 'model_predict',
43
57
  description: MODEL_PREDICT_DESCRIPTION,
@@ -10,16 +10,63 @@
10
10
  * `myvillage dataset append` command uses — so agents and humans write
11
11
  * identically. Raw data stays local; nothing leaves the machine.
12
12
  *
13
- * Tool contract mirrors @myvillage-studio/dataset-schema DATASET_APPEND_TOOL.
13
+ * `dataset_read` is its read side: it wraps readRows() so agents can look at
14
+ * what's been collected (to answer questions about it, avoid duplicate
15
+ * appends, or feed rows to model_predict). Also local-only.
16
+ *
17
+ * Tool contracts mirror @myvillage-studio/dataset-schema DATASET_APPEND_TOOL
18
+ * and DATASET_READ_TOOL.
14
19
  */
15
20
  import { tool } from 'ai';
16
21
  import { z } from 'zod';
17
- import { appendRows, datasetExists, readManifest, ingestAsset } from '../utils/dataset-workspace.js';
18
- import { DATASET_APPEND_DESCRIPTION, MODEL_PREDICT_DESCRIPTION } from './builtin-tools-registry.js';
22
+ import {
23
+ appendRows,
24
+ datasetExists,
25
+ readManifest,
26
+ readRows,
27
+ ingestAsset,
28
+ getAssetPath,
29
+ listDatasets,
30
+ } from '../utils/dataset-workspace.js';
31
+ import {
32
+ DATASET_APPEND_DESCRIPTION,
33
+ DATASET_READ_DESCRIPTION,
34
+ MODEL_PREDICT_DESCRIPTION,
35
+ } from './builtin-tools-registry.js';
19
36
  import { readToolsYaml } from '../utils/local-agent.js';
20
37
 
21
38
  export { BUILTIN_TOOL_DEFS } from './builtin-tools-registry.js';
22
39
 
40
+ const READ_DEFAULT_LIMIT = 50;
41
+ const READ_MAX_LIMIT = 200;
42
+ // Long text rows are clipped so one page can't blow out the model's context.
43
+ const READ_MAX_TEXT_CHARS = 2000;
44
+
45
+ /** Shape a stored row for the model: readable features, no internal noise. */
46
+ function presentRow(slug, manifest, row) {
47
+ let features = row.features;
48
+ let truncated = false;
49
+ if (features && typeof features === 'object' && !Array.isArray(features) && features.path) {
50
+ // AssetRef → absolute path, the same form model_predict / dataset_append take
51
+ features = getAssetPath(slug, features.path);
52
+ } else if (
53
+ Array.isArray(features) &&
54
+ manifest.featureNames?.length === features.length
55
+ ) {
56
+ features = Object.fromEntries(manifest.featureNames.map((name, i) => [name, features[i]]));
57
+ } else if (typeof features === 'string' && features.length > READ_MAX_TEXT_CHARS) {
58
+ features = features.slice(0, READ_MAX_TEXT_CHARS);
59
+ truncated = true;
60
+ }
61
+ const out = { id: row.id, features, split: row.split };
62
+ if (truncated) out.truncated = true;
63
+ if (row.label != null) out.label = row.label;
64
+ if (row.target != null) out.target = row.target;
65
+ if (row.provenance?.source) out.source = row.provenance.source;
66
+ if (row.provenance?.ts) out.addedAt = row.provenance.ts;
67
+ return out;
68
+ }
69
+
23
70
  /**
24
71
  * Gate built-ins by the SAME allowlist as the MyVillageOS platform server
25
72
  * (`allowed_tools` on whichever tools.yaml server points at a myvillageproject.ai
@@ -128,6 +175,77 @@ export function getBuiltinTools(agentName, config) {
128
175
  },
129
176
  }),
130
177
 
178
+ dataset_read: tool({
179
+ description: DATASET_READ_DESCRIPTION,
180
+ inputSchema: z.object({
181
+ datasetId: z.string().describe('Slug of the local dataset to read'),
182
+ version: z
183
+ .number()
184
+ .int()
185
+ .positive()
186
+ .optional()
187
+ .describe('Frozen version number to read; omit for the current working set'),
188
+ split: z.enum(['train', 'val']).optional().describe('Only rows in this split'),
189
+ label: z.string().optional().describe('Only rows with this class label (classification datasets)'),
190
+ offset: z.number().int().min(0).optional().describe('Rows to skip (default 0)'),
191
+ limit: z
192
+ .number()
193
+ .int()
194
+ .min(0)
195
+ .max(READ_MAX_LIMIT)
196
+ .optional()
197
+ .describe(`Rows to return (default ${READ_DEFAULT_LIMIT}, max ${READ_MAX_LIMIT}; 0 = summary only)`),
198
+ }),
199
+ execute: async ({ datasetId, version, split, label, offset = 0, limit = READ_DEFAULT_LIMIT }) => {
200
+ const manifest = readManifest(datasetId);
201
+ if (!manifest) {
202
+ let available = [];
203
+ try {
204
+ available = listDatasets().map((d) => d.slug);
205
+ } catch { /* store unavailable */ }
206
+ return {
207
+ error:
208
+ `Dataset "${datasetId}" not found locally.` +
209
+ (available.length ? ` Available: ${available.join(', ')}.` : ' No local datasets exist yet.'),
210
+ };
211
+ }
212
+ if (version && !manifest.versions?.some((v) => v.version === version)) {
213
+ const known = (manifest.versions ?? []).map((v) => v.version);
214
+ return {
215
+ error:
216
+ `Dataset "${datasetId}" has no version ${version}.` +
217
+ (known.length ? ` Frozen versions: ${known.join(', ')}.` : ' It has no frozen versions yet.'),
218
+ };
219
+ }
220
+ try {
221
+ let rows = readRows(datasetId, { version, split });
222
+ if (label != null) rows = rows.filter((r) => r.label === label);
223
+ const page = rows.slice(offset, offset + limit);
224
+ return {
225
+ ok: true,
226
+ datasetId: manifest.slug,
227
+ name: manifest.name,
228
+ ...(manifest.description ? { description: manifest.description } : {}),
229
+ modality: manifest.modality,
230
+ taskType: manifest.taskType,
231
+ labels: manifest.labels ?? [],
232
+ ...(manifest.featureNames ? { columns: manifest.featureNames } : {}),
233
+ ...(manifest.featureTypes ? { columnTypes: manifest.featureTypes } : {}),
234
+ stats: manifest.stats,
235
+ versions: (manifest.versions ?? []).map((v) => ({ version: v.version, rowCount: v.rowCount, frozenAt: v.frozenAt })),
236
+ reading: version ? `version ${version}` : 'working set',
237
+ matchingRows: rows.length,
238
+ offset,
239
+ returned: page.length,
240
+ hasMore: offset + page.length < rows.length,
241
+ rows: page.map((r) => presentRow(datasetId, manifest, r)),
242
+ };
243
+ } catch (e) {
244
+ return { error: e.message };
245
+ }
246
+ },
247
+ }),
248
+
131
249
  model_predict: tool({
132
250
  description: MODEL_PREDICT_DESCRIPTION,
133
251
  inputSchema: z.object({
@@ -34,13 +34,14 @@ export async function gatherContext(agentConfig, lastCheckIn, recentActions = []
34
34
  }
35
35
  }
36
36
 
37
- // Local Lab datasets & models, so the agent knows what it can append to /
38
- // predict with (and the exact slugs to pass to dataset_append / model_predict).
37
+ // Local Lab datasets & models, so the agent knows what it can read / append
38
+ // to / predict with (and the exact slugs to pass to dataset_read /
39
+ // dataset_append / model_predict).
39
40
  // Local-only, synchronous file reads; tfjs is NOT loaded by listing models.
40
41
  try {
41
42
  const datasets = listDatasets();
42
43
  if (datasets.length > 0) {
43
- parts.push('\nLocal datasets you can add to with dataset_append (use the slug as datasetId):');
44
+ parts.push('\nLocal datasets you can read with dataset_read or add to with dataset_append (use the slug as datasetId):');
44
45
  for (const d of datasets.slice(0, 25)) {
45
46
  parts.push(`- ${d.slug} — ${d.modality}/${d.taskType}, ${d.stats?.rowCount ?? 0} rows`);
46
47
  }
@@ -141,7 +141,8 @@ function requestHostContext(timeoutMs) {
141
141
  // dev server.
142
142
 
143
143
  const DEFAULT_DEV_SERVER = 'https://myvillage-realtime-staging.myvillage-realtime.workers.dev';
144
- const CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
144
+ // No vowels (or look-alikes), so random codes can't spell words.
145
+ const CODE_ALPHABET = 'BCDFGHJKMNPQRSTVWXZ23456789';
145
146
 
146
147
  export class MultiplayerError extends Error {
147
148
  constructor(code, message, status) {
@@ -185,7 +186,8 @@ class Multiplayer {
185
186
  this._ticket = null;
186
187
  /** The current match (see MatchView in the platform docs), or null. */
187
188
  this.match = null;
188
- this.me = { displayName: ctx ? ctx.player.displayName : options.name || 'Player ' + randomCode().slice(0, 3) };
189
+ // Digits only: random letters can spell words.
190
+ this.me = { displayName: ctx ? ctx.player.displayName : options.name || 'Player ' + (100 + Math.floor(Math.random() * 900)) };
189
191
  }
190
192
 
191
193
  // ── lobby plumbing ──
@@ -208,8 +210,14 @@ class Multiplayer {
208
210
  async quickMatch({ lobby } = {}) {
209
211
  this._requireAvailable();
210
212
  // Finished match rooms can't be reused, so dev quick match rotates rooms
211
- // every 10 minutes; tabs that press it within the same window meet.
212
- if (this.mode === 'dev') return this._devMatch('QUICK' + Math.floor(Date.now() / 600000).toString(36).toUpperCase(), false, lobby);
213
+ // every 10 minutes, and connect() steps through numbered rooms in that
214
+ // window past any that are finished or full. Tabs follow the same
215
+ // sequence, so they still meet.
216
+ if (this.mode === 'dev') {
217
+ this._devQuickBase = 'QUICK' + Math.floor(Date.now() / 600000).toString(36).toUpperCase();
218
+ this._devQuickTry = 0;
219
+ return this._devMatch(this._devQuickBase, false, lobby);
220
+ }
213
221
  return this._enter(await this._api('POST', `/api/v1/games/${this._game()}/matches/quick`, { platform: this._ctx.platform, lobby }));
214
222
  }
215
223
 
@@ -294,10 +302,23 @@ class Multiplayer {
294
302
  async connect() {
295
303
  this._requireAvailable();
296
304
  if (!this.match) throw new MultiplayerError('no_match', 'Host, join or quick match first.');
297
- const ticket = await this._freshTicket();
298
305
  const { ArenaClient } = await import('./myvillage-realtime.js');
299
- const net = new ArenaClient({ wsUrl: ticket.wsUrl, ticket: ticket.ticket });
300
- await net.connect();
306
+ let net;
307
+ for (;;) {
308
+ const ticket = await this._freshTicket();
309
+ net = new ArenaClient({ wsUrl: ticket.wsUrl, ticket: ticket.ticket });
310
+ try {
311
+ await net.connect();
312
+ break;
313
+ } catch (err) {
314
+ // Dev quick match: that room is finished or full — try the next one.
315
+ const devQuick = this.mode === 'dev' && this._devQuickBase && this.match.code.startsWith(this._devQuickBase);
316
+ if (!devQuick || this._devQuickTry >= 20 || !/finished|full/i.test(String(err && err.message))) throw err;
317
+ this._devQuickTry++;
318
+ this._ticket = null;
319
+ this._devMatch(this._devQuickBase + '-' + this._devQuickTry, false, this.match.lobby);
320
+ }
321
+ }
301
322
  this._ticket = null; // single use: a reconnect fetches a fresh one
302
323
  this._net = net;
303
324
  return net;
@@ -25,7 +25,7 @@ const TOOL_GROUPS = {
25
25
  villages: ['village_search', 'village_view', 'village_create', 'village_update'],
26
26
  moments: ['moment_create', 'moment_get', 'pulse_create', 'pulse_list'],
27
27
  villagers: ['villager_me', 'villager_search', 'villager_update', 'agent_list'],
28
- meetings: ['recall_send_bot', 'recall_meeting_attendance'],
28
+ meetings: ['recall_send_bot', 'recall_list_sessions', 'recall_meeting_attendance'],
29
29
  };
30
30
 
31
31
  // ─── Agentic App Template (Next.js) ─────────────────────────────────────────
@@ -756,7 +756,7 @@ const TOOL_GROUPS = {
756
756
  villages: ['village_search', 'village_view', 'village_create', 'village_update'],
757
757
  moments: ['moment_create', 'moment_get', 'pulse_create', 'pulse_list'],
758
758
  villagers: ['villager_me', 'villager_search', 'villager_update', 'agent_list'],
759
- meetings: ['recall_send_bot', 'recall_meeting_attendance'],
759
+ meetings: ['recall_send_bot', 'recall_list_sessions', 'recall_meeting_attendance'],
760
760
  };
761
761
 
762
762
  // Tool groups selected during project creation