amorphous-cli 0.1.10 → 0.1.12

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.
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url'
5
5
  import { homedir } from 'node:os'
6
6
  import { CLI_VERSION } from './version.mjs'
7
7
 
8
- export const CONTRACT_VERSION = 10
8
+ export const CONTRACT_VERSION = 13
9
9
 
10
10
  const skillRoot = join(dirname(fileURLToPath(import.meta.url)), '../skill/amorphous')
11
11
 
package/lib/api.mjs CHANGED
@@ -2,8 +2,6 @@ import { cliAppFetchOrigin, cliAppHostHeader } from './config.mjs'
2
2
  import { ensureFreshSession } from './session.mjs'
3
3
 
4
4
  const FRIENDLY = {
5
- 'You already own 2 abstracts': 'You already own 2 abstracts. Run: amorphous-cli list',
6
- 'This abstract already has 8 people': 'This abstract already has 8 people. Run: amorphous-cli people',
7
5
  'User not found': 'No account with that email yet.',
8
6
  'Grant failed': 'Could not invite. Run: amorphous-cli people',
9
7
  'Cannot revoke owner': "You can't remove the owner. Run: amorphous-cli people",
@@ -27,6 +25,12 @@ export function friendlyMessage(raw) {
27
25
  if (FRIENDLY[text]) return FRIENDLY[text]
28
26
  const stripped = text.replace(/^\d{3}\s+/, '')
29
27
  if (FRIENDLY[stripped]) return FRIENDLY[stripped]
28
+ if (/^You already own \d+ abstracts$/.test(stripped)) {
29
+ return `${stripped}. Run: amorphous-cli list`
30
+ }
31
+ if (/^This abstract already has \d+ people$/.test(stripped)) {
32
+ return `${stripped}. Run: amorphous-cli people`
33
+ }
30
34
  return stripped || text || 'Request failed.'
31
35
  }
32
36
 
package/lib/commands.mjs CHANGED
@@ -64,6 +64,12 @@ function printJson(data) {
64
64
  console.log(JSON.stringify(data, null, 2))
65
65
  }
66
66
 
67
+ function warnIfRemoteChanged(detail) {
68
+ if (detail?.remoteChangedSincePush) {
69
+ console.error('This abstract has changed since you last pushed.')
70
+ }
71
+ }
72
+
67
73
  function readMeta(cwd) {
68
74
  const path = projectMetaPath(cwd)
69
75
  if (!existsSync(path)) return null
@@ -199,6 +205,11 @@ async function cmdSetup(rest) {
199
205
  }
200
206
  console.log('Installed the Amorphous skill.')
201
207
  for (const dir of dirs ?? installUserSkill()) console.log(dir)
208
+ console.log(`
209
+ Tell the person:
210
+ They describe what they want made. You make it.
211
+ Prefer amorphous-cli serve while you edit. Open the URL and leave it running. It reloads when the files change.
212
+ They tell you when to push, preview, publish, or invite.`)
202
213
  }
203
214
 
204
215
  async function cmdCreate(cwd, rest) {
@@ -225,7 +236,10 @@ async function cmdCreate(cwd, rest) {
225
236
  writeStarter(cwd)
226
237
  syncProjectContract(cwd)
227
238
  if (jsonMode(flags)) printJson({ id, name: name || 'Untitled' })
228
- else console.log(id)
239
+ else {
240
+ console.log(id)
241
+ console.log('Prefer amorphous-cli serve while you edit.')
242
+ }
229
243
  }
230
244
 
231
245
  function takeAccess(flags, positional) {
@@ -266,8 +280,26 @@ async function cmdInvite(cwd, rest) {
266
280
  })
267
281
  const url = String(result.url || '')
268
282
  if (!url) throw new Error('Could not invite.')
269
- if (jsonMode(flags)) printJson({ email, access: role === 'user' ? 'view' : role === 'editor' ? 'edit' : role, url })
270
- else console.log(url)
283
+ const accessLabel = role === 'user' ? 'view' : role === 'editor' ? 'edit' : role
284
+ if (jsonMode(flags)) {
285
+ printJson({ email, access: accessLabel, url, mailed: Boolean(result.mailed) })
286
+ return
287
+ }
288
+ console.log(url)
289
+ if (result.notified === 'role') console.log('Updated their access.')
290
+ else if (result.mailed) console.log('Emailed the invite.')
291
+ else console.log('The invite is saved. The email did not send.')
292
+ }
293
+
294
+ async function cmdNotify(cwd, rest) {
295
+ const { flags, positional } = parseFlags(rest)
296
+ if (flags.help) return printHelp('notify')
297
+ const id = await resolveAbstractId(cwd, flags, positional)
298
+ const result = await apiFetch(`/api/abstracts/${id}/notify`, { method: 'POST' })
299
+ const sent = Number(result.sent || 0)
300
+ if (jsonMode(flags)) printJson({ sent })
301
+ else if (sent === 0) console.log('No one else to email.')
302
+ else console.log(sent === 1 ? 'Emailed 1 member.' : `Emailed ${sent} members.`)
271
303
  }
272
304
 
273
305
  async function cmdRemove(cwd, rest) {
@@ -313,6 +345,7 @@ async function cmdPull(cwd, rest) {
313
345
  if (flags.help) return printHelp('pull')
314
346
  const id = requireMeta(cwd)
315
347
  const detail = await apiFetch(`/api/abstracts/${id}`)
348
+ warnIfRemoteChanged(detail)
316
349
  const records = await apiFetch(`/api/abstracts/${id}/records`)
317
350
  const rows = records.records || []
318
351
  const shared = rows.filter((r) => r.visibility !== 'private')
@@ -326,12 +359,14 @@ async function cmdPull(cwd, rest) {
326
359
  })
327
360
  syncProjectContract(cwd)
328
361
  console.log('Pulled.')
362
+ console.log('Prefer amorphous-cli serve while you edit.')
329
363
  }
330
364
 
331
365
  async function cmdPush(cwd, rest) {
332
366
  const { flags } = parseFlags(rest)
333
367
  if (flags.help) return printHelp('push')
334
368
  const id = requireMeta(cwd)
369
+ warnIfRemoteChanged(await apiFetch(`/api/abstracts/${id}`))
335
370
  if (flags.data) {
336
371
  const localRecords = recordsFromLocal(cwd)
337
372
  const { refused } = await apiFetch(`/api/abstracts/${id}/records`, {
@@ -364,8 +399,27 @@ async function cmdServe(cwd, rest) {
364
399
  port,
365
400
  json: jsonMode(flags),
366
401
  })
402
+ let warnedRemote = false
403
+ async function watchRemote() {
404
+ try {
405
+ const detail = await apiFetch(`/api/abstracts/${id}`)
406
+ if (detail.remoteChangedSincePush && !warnedRemote) {
407
+ warnedRemote = true
408
+ console.error('This abstract has changed since you last pushed.')
409
+ } else if (!detail.remoteChangedSincePush) {
410
+ warnedRemote = false
411
+ }
412
+ } catch {
413
+ /* leave the dev server up if the check cannot reach the abstract */
414
+ }
415
+ }
416
+ void watchRemote()
417
+ const remoteTimer = setInterval(() => {
418
+ void watchRemote()
419
+ }, 20000)
367
420
  await new Promise((resolve) => {
368
421
  const stop = () => {
422
+ clearInterval(remoteTimer)
369
423
  running.close().then(resolve, resolve)
370
424
  }
371
425
  process.once('SIGINT', stop)
@@ -394,8 +448,10 @@ async function cmdPreview(cwd, rest) {
394
448
  const { flags, positional } = parseFlags(rest)
395
449
  if (flags.help) return printHelp('preview')
396
450
  const id = await resolveAbstractId(cwd, flags, positional)
451
+ const detail = await apiFetch(`/api/abstracts/${id}`)
452
+ warnIfRemoteChanged(detail)
397
453
  const urls = urlsFor(id)
398
- if (jsonMode(flags)) printJson({ preview: urls.preview })
454
+ if (jsonMode(flags)) printJson({ preview: urls.preview, remoteChangedSincePush: Boolean(detail.remoteChangedSincePush) })
399
455
  else console.log(urls.preview)
400
456
  }
401
457
 
@@ -403,6 +459,7 @@ async function cmdPublish(cwd, rest) {
403
459
  const { flags, positional } = parseFlags(rest)
404
460
  if (flags.help) return printHelp('publish')
405
461
  const id = await resolveAbstractId(cwd, flags, positional)
462
+ warnIfRemoteChanged(await apiFetch(`/api/abstracts/${id}`))
406
463
  const live = Boolean(flags.live)
407
464
  const version = await apiFetch(`/api/abstracts/${id}/publish`, {
408
465
  method: 'POST',
@@ -752,6 +809,8 @@ export async function runCommand(argv, cwd = process.cwd()) {
752
809
  return cmdCreate(cwd, rest)
753
810
  case 'invite':
754
811
  return cmdInvite(cwd, rest)
812
+ case 'notify':
813
+ return cmdNotify(cwd, rest)
755
814
  case 'remove':
756
815
  return cmdRemove(cwd, rest)
757
816
  case 'people':
package/lib/help.mjs CHANGED
@@ -7,13 +7,14 @@ export const ROOT_HELP = `Amorphous
7
7
  create New abstract
8
8
  open [abstract] Open URL
9
9
  preview [abstract] Preview URL (draft)
10
- serve Serve this folder locally
10
+ serve Develop in this folder
11
11
  pull Pull draft and records to this folder
12
12
  push Push local files
13
13
  publish [--live] Publish a version; --live is Go live
14
14
  live [abstract] Make a version live
15
15
  visibility anyone|members Anyone can view / Members only
16
- invite <email> Print the link to give them
16
+ invite <email> Email an invite and print the link
17
+ notify Email members about the published version
17
18
  remove <email|id> Remove
18
19
  people [abstract] People
19
20
  tag / untag Tag
@@ -67,7 +68,7 @@ Print the Preview URL (draft).`,
67
68
 
68
69
  serve: `serve [--port <n>] [--json]
69
70
 
70
- Serve this folder on 127.0.0.1 and reload when index.html, styles.css, or app.js changes.
71
+ Preferred way to develop in this folder. Serves it on 127.0.0.1 and reloads when index.html, styles.css, or app.js changes. Leave it running and open the URL.
71
72
  Records start local, in this folder. The page can switch to prod, which reads and writes the abstract's records.`,
72
73
 
73
74
  pull: `pull
@@ -93,7 +94,11 @@ Anyone can view, or Members only. Uses the live version if you omit --version.`,
93
94
 
94
95
  invite: `invite <email> [view|use|edit] [--abstract <id|name>] [--json]
95
96
 
96
- Save an invite for this email and print the link to give them. Access: view, use, or edit. Default: view.`,
97
+ Email an invite and print the link. Access: view, use, or edit. Default: view.`,
98
+
99
+ notify: `notify [abstract] [--json]
100
+
101
+ Email members that the published version changed. Does not publish. You are not emailed.`,
97
102
 
98
103
  remove: `remove <email|id> [--abstract <id|name>]
99
104
 
package/mcp.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Local MCP. Six tools for abstracts.
3
+ * Local MCP. Tools for abstracts.
4
4
  * Stdout is the protocol. The CLI runs as a child so its output stays off this stream.
5
5
  */
6
6
  import { spawn } from 'node:child_process'
@@ -72,7 +72,7 @@ await serveStdio(() => {
72
72
  {
73
73
  title: 'Pull the abstract',
74
74
  description:
75
- 'Download the abstract files and records into this folder. Overwrites the local copies of those files.',
75
+ 'Download the abstract files and records into this folder. Overwrites the local copies of those files. Prefer amorphous-cli serve while editing; it reloads as the files change.',
76
76
  inputSchema: z.object({}),
77
77
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
78
78
  },
@@ -122,7 +122,7 @@ await serveStdio(() => {
122
122
  {
123
123
  title: 'Invite someone',
124
124
  description:
125
- 'Save an invite for this email and return the link to give them. This does not send an email. access is view (can open), use (can write shared records), or edit (can change the abstract).',
125
+ 'Email an invite and return the link. access is view (can open), use (can write shared records), or edit (can change the abstract).',
126
126
  inputSchema: z.object({
127
127
  email: z.string().describe('Email address'),
128
128
  access: z.enum(['view', 'use', 'edit']).describe('view, use, or edit'),
@@ -132,5 +132,17 @@ await serveStdio(() => {
132
132
  async ({ email, access }) => call(['invite', email, access, '--json']),
133
133
  )
134
134
 
135
+ server.registerTool(
136
+ 'notify',
137
+ {
138
+ title: 'Email members about the published version',
139
+ description:
140
+ 'Email members that the published version changed. On demand. This does not publish. The person running it is not emailed.',
141
+ inputSchema: z.object({}),
142
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
143
+ },
144
+ async () => call(['notify', '--json']),
145
+ )
146
+
135
147
  return server
136
148
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amorphous-cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "mcpName": "io.github.amorphous-one/amorphous",
5
5
  "description": "Make whatever you want, yourself, with your own agent, or with others.",
6
6
  "type": "module",
@@ -30,6 +30,14 @@ A running abstract provides `amorphous` in `app.js`:
30
30
 
31
31
  A private record belongs to the signed-in person who wrote it. The abstract does not read `private.ndjson`.
32
32
 
33
+ ## Person
34
+
35
+ After `setup`, tell them this, briefly. `setup` also prints it. Say it to them in your own words.
36
+
37
+ They describe what they want made. You make it.
38
+ Prefer `amorphous-cli serve` while you edit. Open the URL and leave it running. It reloads when the files change, and it is the easiest way for them to look at the work.
39
+ They tell you when to push, preview, publish, or invite. Do those when they ask.
40
+
33
41
  ## Loop
34
42
 
35
43
  Install the CLI once with `npm install -g amorphous-cli@latest`, then call it as `amorphous-cli <command>`. If a command prints `Run: npm install -g amorphous-cli@latest && amorphous-cli setup`, run that line, then repeat the command.
@@ -37,11 +45,15 @@ Install the CLI once with `npm install -g amorphous-cli@latest`, then call it as
37
45
  1. If you are not signed in, run `amorphous-cli login`. Show the person the link it prints and ask them to finish in the browser. Do not ask for an email or a code.
38
46
  2. `amorphous-cli create --name "..."` in a folder, or `amorphous-cli pull` when `.amorphous.json` is already there.
39
47
  3. Edit the files to match what they asked for. `me` and `names` are how the abstract knows who is here.
40
- 4. `amorphous-cli serve` and open that URL. It reloads when the files change. Records on that page start in this folder. Switch the page to prod to read and write the abstract's records.
41
- 5. `amorphous-cli push` sends the source. `amorphous-cli push --data` also sends the records in this folder.
42
- 6. `amorphous-cli preview` and open that URL.
43
- 7. `amorphous-cli publish --live`
44
- 8. `amorphous-cli invite <email> use` or `edit` or `view`. It prints a link and does not send an email. Give them that link. They open it and sign in with that email.
48
+ 4. Prefer `amorphous-cli serve` while you edit. Open that URL and leave it running. It reloads when the files change, and it is the easiest way to check the work. Records on that page start in this folder. Switch the page to prod to read and write the abstract's records. You can push and preview without it.
49
+
50
+ When they ask:
51
+
52
+ - `amorphous-cli push` sends the source. `amorphous-cli push --data` also sends the records in this folder.
53
+ - `amorphous-cli preview` and open that URL.
54
+ - `amorphous-cli publish --live`
55
+ - `amorphous-cli invite <email> use` or `edit` or `view`. It emails them a link and prints that link. They open it and sign in with that email.
56
+ - `amorphous-cli notify` emails members that the published version changed. It does not publish. Do that only when they ask.
45
57
 
46
58
  `amorphous-cli visibility anyone` lets anyone view and write shared records. `amorphous-cli visibility members` keeps the abstract to invited people.
47
59