parley-live 0.4.2 → 0.5.1

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.
Files changed (32) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +8 -2
  3. package/dist/lens.js +252 -0
  4. package/kb/index/bm25.json +1 -0
  5. package/kb/index/chunks-meta.jsonl +11523 -0
  6. package/lens-builder/build-from-prompt.mjs +19 -0
  7. package/lens-builder/build.mjs +147 -0
  8. package/lens-builder/cli.mjs +103 -0
  9. package/lens-builder/kb/lib-fetch.mjs +35 -0
  10. package/lens-builder/kb/search.mjs +61 -0
  11. package/lens-builder/kb/show.mjs +4 -0
  12. package/lens-builder/kb/tokenize.mjs +27 -0
  13. package/lens-builder/lib/bridge.mjs +186 -0
  14. package/lens-builder/lib/submission.mjs +246 -0
  15. package/lens-builder/mcp-client.mjs +101 -0
  16. package/lens-builder/pitch/one-pager.mjs +70 -0
  17. package/lens-builder/planner.mjs +108 -0
  18. package/lens-builder/publish.mjs +127 -0
  19. package/lens-builder/training/author-scene.ts +77 -0
  20. package/lens-builder/training/compile.mjs +258 -0
  21. package/lens-builder/vendor/specs-publish/LICENSE +208 -0
  22. package/lens-builder/vendor/specs-publish/NOTICE +13 -0
  23. package/lens-builder/vendor/specs-publish/scripts/make-eec-script.py +85 -0
  24. package/lens-builder/vendor/specs-publish/scripts/package.ts +189 -0
  25. package/lens-builder/vendor/specs-publish/scripts/preflight.ts +852 -0
  26. package/lens-builder/vendor/specs-publish/scripts/set-lens-icon.ts +107 -0
  27. package/lens-builder/vendor/specs-publish/scripts/set-lens-name.ts +121 -0
  28. package/lens-builder/vendor/specs-publish/scripts/set-signing-key.ts +129 -0
  29. package/lens-builder/vendor/specs-publish/scripts/signing-key.js +290 -0
  30. package/lens-builder/verify.mjs +186 -0
  31. package/lens-builder/webxr/generate.mjs +238 -0
  32. package/package.json +11 -5
package/README.md CHANGED
@@ -20,6 +20,7 @@ parley ugc-ad --product product.jpg --name "Cold Brew Kit" --actor jordan --mark
20
20
  | `parley voice "<text>" \| --file script.txt` | Narration as a hosted WAV. `--voice warm\|energetic\|documentary\|deep\|bright\|storyteller`, `--quality natural\|fish\|studio`, `--no-fallback`, `--out`. Natural voice: 1 credit per 1,000 characters; studio: 1 per 300. Becomes `@last` for `--narration`. `parley voices` lists personas and prices. |
21
21
  | `parley studio …` | The film pipeline without JSON: `character add\|cast\|list`, `location add\|cast\|list`, `shot add\|list`, `render <shotId> [--wait]`, `assemble`, `reels`. Every subcommand takes `--project proj_…`. |
22
22
  | `parley deploy <dir> --name <slug>` | Ship a built static site (index.html at the root) to a live URL. Reuse `--project proj_…` to redeploy the same site at the same URL. Free. |
23
+ | `parley lens <status|connect|build|describe|record|metrics>` | A Snap Specs Lens from a work procedure, built and tested on this computer through Lens Studio: a hands-free step-by-step guide with pinch buttons and optional voice shortcuts. `status` says exactly what to set up; `build <form.json>` is free; `describe "..."` plans the steps from prose (about a cent, billed to your credits). Every build is checked in the Lens Studio preview; what only glasses can judge is marked, never passed. |
23
24
  | `parley login [--no-browser]` | Device-code sign-in. Prints a link, you sign in once in the browser, the CLI receives a session token. `--no-browser` prints the link instead of opening it (cloud sandboxes, SSH). |
24
25
  | `parley account` | Credit balance. |
25
26
  | `parley engines` | Which video engines this server can run right now, with credit rates. |
package/dist/index.js CHANGED
@@ -17,7 +17,8 @@
17
17
  import { readFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import { api, authWebUrl, backendUrl, clearCredentials, CliError, downloadTo, fileToDataUri, isUrl, loadCredentials, loadLast, openInBrowser, rememberLast, resolveRef, saveCredentials, sleep, } from './client.js';
20
- const VERSION = '0.4.2';
20
+ import { cmdLens, LensError } from './lens.js';
21
+ const VERSION = '0.5.1';
21
22
  /** Server advice written for Parley's in-app agent names its tools; a CLI user
22
23
  * has commands instead, so the same advice is re-spoken in CLI terms. */
23
24
  function humanizeServerMessage(message) {
@@ -1004,6 +1005,7 @@ function help(topic) {
1004
1005
  engines: 'parley engines\n Which video engines this server can run right now and their credit rates.',
1005
1006
  'camera-moves': 'parley camera-moves\n The named camera-move presets accepted by `parley video --move`.',
1006
1007
  account: 'parley account\n Your credit balance.',
1008
+ lens: 'parley lens <status|connect|build|describe|publish|record|metrics>\n Build a Snap Specs Lens on this computer through Lens Studio: a step-by-step training guide with pinch buttons and optional voice shortcuts, tested in the preview before you see it. "describe" turns plain language into steps (about a cent, billed to your credits); "build <form.json>" is free. Run `parley lens status` first: it tells you exactly what to set up. `publish` runs Snap\u0027s own preflight and SnapOS Package export (needs Lens Studio signed in).',
1007
1009
  };
1008
1010
  if (topic && lines[topic]) {
1009
1011
  console.log(lines[topic]);
@@ -1077,6 +1079,10 @@ async function main(argv) {
1077
1079
  case 'deploy':
1078
1080
  await cmdDeploy(p);
1079
1081
  break;
1082
+ case 'lens':
1083
+ case 'specs':
1084
+ await cmdLens(p.positionals, p.flags, JSON_MODE);
1085
+ break;
1080
1086
  case 'help':
1081
1087
  case '--help':
1082
1088
  case '-h':
@@ -1088,7 +1094,7 @@ async function main(argv) {
1088
1094
  return 0;
1089
1095
  }
1090
1096
  catch (e) {
1091
- const code = e instanceof CliError ? e.exitCode : 1;
1097
+ const code = e instanceof CliError || e instanceof LensError ? e.exitCode : 1;
1092
1098
  const message = humanizeServerMessage(e?.message || String(e));
1093
1099
  if (JSON_MODE)
1094
1100
  console.log(JSON.stringify({ ok: false, error: message, code }));
package/dist/lens.js ADDED
@@ -0,0 +1,252 @@
1
+ /**
2
+ * `parley lens`: build a Snap Specs Lens from a plain-language procedure or a form, on this computer, through
3
+ * Lens Studio. The pipeline (lens-builder/) ships inside this package; it talks only to Lens Studio's local MCP
4
+ * server on 127.0.0.1, and its planning step goes through Parley's proxy on the signed-in account.
5
+ *
6
+ * parley lens status Lens Studio running? connected? build tools present?
7
+ * parley lens connect --config '<json>' Save the config Lens Studio copies (AI Assistant > MCP > Configure Server)
8
+ * parley lens build <form.json> Form -> tested Lens ($0, no model call)
9
+ * parley lens describe "<procedure>" Plain language -> steps (billed, about a cent) -> tested Lens
10
+ * parley lens publish [<lens id>] Snap's preflight, then Snap's SnapOS Package export
11
+ * parley lens record <run> [--on-glasses passed|issues] [--published submitted|published]
12
+ * parley lens metrics
13
+ */
14
+ import { spawn } from 'node:child_process';
15
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { backendUrl, currentToken, homeDir } from './client.js';
19
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
+ const BUILDER = path.join(PKG_ROOT, 'lens-builder');
21
+ export class LensError extends Error {
22
+ exitCode;
23
+ constructor(message, exitCode = 1) {
24
+ super(message);
25
+ this.exitCode = exitCode;
26
+ }
27
+ }
28
+ const str = (v) => (typeof v === 'string' ? v : Array.isArray(v) ? v[0] : undefined);
29
+ function mcpConfigPath() { return path.join(homeDir(), 'lens-studio-mcp.json'); }
30
+ /** Accepts the JSON Lens Studio copies to the clipboard, or a bare { url, token }. Loopback only. */
31
+ export function parseMcpConfig(raw) {
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(raw);
35
+ }
36
+ catch {
37
+ throw new LensError('That does not look like the config Lens Studio copies. In Lens Studio: AI Assistant > AI Model Context Protocol (MCP) > Configure Server > Copy MCP Config, then pass it with --config.', 65);
38
+ }
39
+ const server = parsed?.mcpServers ? Object.values(parsed.mcpServers)[0] : parsed;
40
+ const url = server?.url;
41
+ const auth = server?.headers?.Authorization ?? (server?.token ? `Bearer ${server.token}` : undefined);
42
+ if (!url || !auth)
43
+ throw new LensError('The config is missing the server address or the connection code.', 65);
44
+ const host = new URL(url).hostname;
45
+ if (host !== 'localhost' && host !== '127.0.0.1')
46
+ throw new LensError('Only a Lens Studio running on this computer can be connected.', 65);
47
+ return { url, token: auth.replace(/^Bearer\s+/i, '') };
48
+ }
49
+ function loadMcpConfig() {
50
+ try {
51
+ return existsSync(mcpConfigPath()) ? JSON.parse(readFileSync(mcpConfigPath(), 'utf8')) : null;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ function dataDir() { return process.env.PARLEY_LENS_DATA_DIR || path.join(homeDir(), 'lens-builder'); }
58
+ function runPipeline(command, input, onEvent) {
59
+ if (!existsSync(path.join(BUILDER, 'cli.mjs')))
60
+ throw new LensError('This install of parley-live is missing the Lens Builder. Reinstall: npm install -g parley-live@latest', 70);
61
+ const cfg = loadMcpConfig();
62
+ const env = { ...process.env, PARLEY_LENS_DATA_DIR: dataDir(), PARLEY_REQUIRE_PROXY: '1' };
63
+ if (cfg) {
64
+ env.LENS_MCP_URL = cfg.url;
65
+ env.LENS_MCP_TOKEN = cfg.token;
66
+ }
67
+ const token = currentToken();
68
+ if (token) {
69
+ env.PARLEY_PROXY_URL = backendUrl();
70
+ env.PARLEY_PROXY_TOKEN = token;
71
+ }
72
+ return new Promise((resolve, reject) => {
73
+ const child = spawn(process.execPath, [path.join(BUILDER, 'cli.mjs'), command], { env, cwd: PKG_ROOT, windowsHide: true });
74
+ const events = [];
75
+ let buffer = '', stderr = '';
76
+ child.stdout.on('data', (c) => {
77
+ buffer += c.toString('utf8');
78
+ let nl;
79
+ while ((nl = buffer.indexOf('\n')) >= 0) {
80
+ const line = buffer.slice(0, nl).trim();
81
+ buffer = buffer.slice(nl + 1);
82
+ if (!line)
83
+ continue;
84
+ try {
85
+ const e = JSON.parse(line);
86
+ events.push(e);
87
+ onEvent?.(e);
88
+ }
89
+ catch { /* not an event */ }
90
+ }
91
+ });
92
+ child.stderr.on('data', (c) => { stderr += c.toString('utf8'); });
93
+ child.on('error', reject);
94
+ child.on('close', () => events.length ? resolve(events) : reject(new LensError(stderr.trim().slice(0, 500) || 'The Lens Builder produced no output.', 70)));
95
+ child.stdin.end(input === undefined ? '' : JSON.stringify(input));
96
+ });
97
+ }
98
+ /** What a person must do next, in order. Empty when everything is ready. */
99
+ export function nextSteps(s) {
100
+ const steps = [];
101
+ if (!s.configured) {
102
+ steps.push('Install Lens Studio 5.22 or later from https://ar.snap.com/download and open it. Create a project (Home > SPECS > Base Template) and leave it open; the MCP server starts with a project open.');
103
+ steps.push('In Lens Studio: AI Assistant > AI Model Context Protocol (MCP) > Configure Server > Copy MCP Config. Then run: parley lens connect --config \'<paste>\'. One time only: Lens Studio keeps the same code in your keychain across restarts.');
104
+ }
105
+ else if (!s.reachable) {
106
+ steps.push(/rejected/i.test(s.error || '') ? 'Lens Studio rejected the saved connection code. Copy a fresh one (AI Assistant > MCP > Configure Server > Copy MCP Config) and run parley lens connect --config again.' : 'Start Lens Studio and open any project (Home > SPECS > Base Template), then run parley lens status again.');
107
+ }
108
+ else if (!s.toolsOk) {
109
+ steps.push(`Lens Studio is missing the build tools (${(s.missingTools || []).join(', ')}). Lens Studio installs Snap's "Core Chat Tools" and "Run Editor Code" packages by itself after a restart while online; if they are still missing, open Window > Asset Library in Lens Studio and install them, then restart Lens Studio.`);
110
+ }
111
+ return steps;
112
+ }
113
+ export async function cmdLens(positionals, flags, jsonMode) {
114
+ const sub = positionals[0];
115
+ const out = (o, human) => { if (jsonMode)
116
+ console.log(JSON.stringify(o));
117
+ else
118
+ human(); };
119
+ if (sub === 'connect') {
120
+ const raw = str(flags.config) ?? (flags.config === undefined && positionals[1] ? positionals[1] : undefined);
121
+ if (!raw)
122
+ throw new LensError('Usage: parley lens connect --config \'<the JSON Lens Studio copied>\'\n In Lens Studio: AI Assistant > AI Model Context Protocol (MCP) > Configure Server > Copy MCP Config.', 64);
123
+ const cfg = parseMcpConfig(raw.startsWith('@') ? readFileSync(raw.slice(1), 'utf8') : raw);
124
+ mkdirSync(homeDir(), { recursive: true });
125
+ writeFileSync(mcpConfigPath(), JSON.stringify(cfg) + '\n', { mode: 0o600 });
126
+ try {
127
+ chmodSync(mcpConfigPath(), 0o600);
128
+ }
129
+ catch { /* windows */ }
130
+ const [s] = await runPipeline('status', undefined);
131
+ const steps = nextSteps(s);
132
+ out({ ok: s.reachable && s.toolsOk, saved: mcpConfigPath(), status: s, nextSteps: steps }, () => {
133
+ console.log(`Saved the connection to ${mcpConfigPath()} (this file only; never share it).`);
134
+ console.log(s.reachable && s.toolsOk ? 'Connected to Lens Studio. Build tools present. Ready to build.' : `Saved, but not ready yet:\n - ${steps.join('\n - ')}`);
135
+ });
136
+ return;
137
+ }
138
+ if (sub === 'status' || sub === undefined) {
139
+ const [s] = await runPipeline('status', undefined);
140
+ const steps = nextSteps(s);
141
+ out({ ok: !!(s.reachable && s.toolsOk), ...s, signedIn: !!currentToken(), nextSteps: steps }, () => {
142
+ console.log(`Lens Studio: ${s.reachable ? 'connected' : s.configured ? 'not answering' : 'not connected yet'}`);
143
+ console.log(`Build tools: ${s.toolsOk ? 'present' : 'missing'}`);
144
+ console.log(`Parley sign-in (needed only for "describe"): ${currentToken() ? 'yes' : 'no, run parley login'}`);
145
+ if (s.project?.file)
146
+ console.log(`Open project: ${s.project.file}`);
147
+ if (steps.length)
148
+ console.log(`\nNext:\n - ${steps.join('\n - ')}`);
149
+ else
150
+ console.log('\nReady. Try: parley lens build <form.json> or parley lens describe "<your procedure>"');
151
+ });
152
+ if (!(s.reachable && s.toolsOk))
153
+ throw new LensError(steps[0] || s.error || 'Lens Studio is not ready.', 3);
154
+ return;
155
+ }
156
+ if (sub === 'build' || sub === 'describe') {
157
+ let form, planner = null;
158
+ if (sub === 'build') {
159
+ const file = positionals[1];
160
+ if (!file)
161
+ throw new LensError('Usage: parley lens build <form.json>\n {"procedureName":"...","steps":[{"instruction":"...","warning":""}],"completionMessage":"...","voiceShortcuts":true}', 64);
162
+ form = JSON.parse(readFileSync(file, 'utf8'));
163
+ }
164
+ else {
165
+ const prompt = positionals.slice(1).join(' ').trim();
166
+ if (!prompt)
167
+ throw new LensError('Usage: parley lens describe "<the procedure, in your own words>"', 64);
168
+ if (!currentToken())
169
+ throw new LensError('Describing a Lens uses a Parley model call. Run parley login first, or write the steps yourself and use parley lens build.', 2);
170
+ const [p] = await runPipeline('plan', { prompt });
171
+ if (p.type === 'error')
172
+ throw new LensError(p.error, /credits/i.test(p.error) ? 4 : 1);
173
+ if (p.decision !== 'build') {
174
+ out({ ok: false, decision: p.decision, question: p.question, reason: p.reason, costUsd: p.costUsd }, () => console.log(p.decision === 'clarify' ? `One question first: ${p.question}` : `This cannot be built as asked: ${p.reason}`));
175
+ throw new LensError(p.decision === 'clarify' ? 'Answer the question and describe it again.' : 'Nothing was built.', 5);
176
+ }
177
+ planner = { ...p, prompt };
178
+ form = p.form;
179
+ if (!jsonMode) {
180
+ console.log(`Planned "${form.procedureName}", ${form.steps.length} step(s), model cost USD ${p.costUsd} (billed to your Parley credits). Building...`);
181
+ }
182
+ if (flags['plan-only']) {
183
+ out({ ok: true, form, costUsd: p.costUsd }, () => console.log(JSON.stringify(form, null, 2)));
184
+ return;
185
+ }
186
+ }
187
+ const events = await runPipeline('build', { form, planner }, (e) => { if (!jsonMode && e.type === 'log')
188
+ console.log(' ' + e.message); });
189
+ const r = events[events.length - 1];
190
+ if (r.type === 'error')
191
+ throw new LensError(r.error, 1);
192
+ out({ ok: r.ok, lens_id: r.lens_id, iterations: r.iterations, seconds: r.seconds, costUsd: r.costUsd, runDir: r.runDir, reportPath: r.reportPath, checks: r.checks }, () => {
193
+ console.log(r.ok ? `\nPASS: ${r.lens_id} passed every preview check in ${r.seconds}s.` : `\nFAIL: ${r.lens_id} needs a person to look at it.`);
194
+ for (const c of r.checks || [])
195
+ console.log(` ${c.status === 'pass' ? 'PASS' : c.status === 'fail' ? 'FAIL' : 'ON GLASSES'} ${c.item}`);
196
+ console.log(`Report: ${r.reportPath}\nNext: pair your Specs in Lens Studio and press Send to Spectacles; publish from Lens Studio.`);
197
+ });
198
+ if (!r.ok)
199
+ throw new LensError('The build did not pass. Read the report.', 6);
200
+ return;
201
+ }
202
+ if (sub === 'publish') {
203
+ const target = positionals[1];
204
+ const events = await runPipeline('publish', { target }, (e) => { if (!jsonMode && e.type === 'log')
205
+ console.log(' ' + e.message); });
206
+ const r = events[events.length - 1];
207
+ if (r.type === 'error')
208
+ throw new LensError(r.error, 1);
209
+ out(r, () => {
210
+ console.log('');
211
+ console.log(`Lens name for Snap: ${r.lensName ?? '(unknown)'}`);
212
+ console.log(`Name, icon and signing key on the project: ${r.readyLocally ? 'all set' : 'incomplete'}`);
213
+ if (r.issues?.length)
214
+ for (const i of r.issues)
215
+ console.log(' - ' + i);
216
+ if (r.ok) {
217
+ console.log(`\nSnapOS Package exported: ${r.spkPath}`);
218
+ if (r.bytes)
219
+ console.log(`${r.bytes} bytes${r.checksum ? ', checksum ' + r.checksum : ''}`);
220
+ }
221
+ else if (r.stage === 'blocked_on_sign_in') {
222
+ console.log('\nNot packaged: Lens Studio is not signed in.');
223
+ }
224
+ for (const n of r.next ?? [])
225
+ console.log('\n' + n);
226
+ });
227
+ if (!r.ok)
228
+ throw new LensError(r.stage === 'blocked_on_sign_in' ? 'Sign in to Lens Studio, then run this again.' : 'The Lens was not packaged.', r.stage === 'blocked_on_sign_in' ? 2 : 6);
229
+ return;
230
+ }
231
+ if (sub === 'record') {
232
+ const run = positionals[1];
233
+ const hw = str(flags['on-glasses']);
234
+ const pub = str(flags.published);
235
+ if (!run || (!hw && !pub && !str(flags.notes)))
236
+ throw new LensError('Usage: parley lens record <run-folder-name> [--on-glasses passed|issues] [--published submitted|published] [--notes "..."]', 64);
237
+ const map = { passed: 'passed on glasses', issues: 'issues found on glasses', untested: 'not tested', submitted: 'submitted', published: 'published', unpublished: 'not published' };
238
+ const [r] = await runPipeline('record', { run, hardwareTest: hw ? map[hw] || hw : undefined, publishStatus: pub ? map[pub] || pub : undefined, notes: str(flags.notes) });
239
+ if (r.type === 'error')
240
+ throw new LensError(r.error, 65);
241
+ out({ ok: true, ...r }, () => console.log(`Recorded for ${r.title}: on glasses = ${r.hardwareTest}, published = ${r.publishStatus}`));
242
+ return;
243
+ }
244
+ if (sub === 'metrics') {
245
+ const [m] = await runPipeline('metrics', undefined);
246
+ out({ ok: true, ...m }, () => { for (const l of m.lenses || [])
247
+ console.log(`${l.pass ? 'PASS' : 'FAIL'} ${String(l.seconds).padStart(4)}s ${l.steps} steps ${l.hardwareTest} ${l.publishStatus} ${l.title} (${l.run})`); if (!(m.lenses || []).length)
248
+ console.log('No Lenses built on this computer yet.'); });
249
+ return;
250
+ }
251
+ throw new LensError(`Unknown: parley lens ${sub}. Use status | connect | build | describe | publish | record | metrics.`, 64);
252
+ }