amorphous-cli 0.1.11 → 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.
package/lib/api.mjs CHANGED
@@ -2,7 +2,6 @@ import { cliAppFetchOrigin, cliAppHostHeader } from './config.mjs'
2
2
  import { ensureFreshSession } from './session.mjs'
3
3
 
4
4
  const FRIENDLY = {
5
- 'This abstract already has 8 people': 'This abstract already has 8 people. Run: amorphous-cli people',
6
5
  'User not found': 'No account with that email yet.',
7
6
  'Grant failed': 'Could not invite. Run: amorphous-cli people',
8
7
  'Cannot revoke owner': "You can't remove the owner. Run: amorphous-cli people",
@@ -29,6 +28,9 @@ export function friendlyMessage(raw) {
29
28
  if (/^You already own \d+ abstracts$/.test(stripped)) {
30
29
  return `${stripped}. Run: amorphous-cli list`
31
30
  }
31
+ if (/^This abstract already has \d+ people$/.test(stripped)) {
32
+ return `${stripped}. Run: amorphous-cli people`
33
+ }
32
34
  return stripped || text || 'Request failed.'
33
35
  }
34
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
@@ -274,8 +280,26 @@ async function cmdInvite(cwd, rest) {
274
280
  })
275
281
  const url = String(result.url || '')
276
282
  if (!url) throw new Error('Could not invite.')
277
- if (jsonMode(flags)) printJson({ email, access: role === 'user' ? 'view' : role === 'editor' ? 'edit' : role, url })
278
- 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.`)
279
303
  }
280
304
 
281
305
  async function cmdRemove(cwd, rest) {
@@ -321,6 +345,7 @@ async function cmdPull(cwd, rest) {
321
345
  if (flags.help) return printHelp('pull')
322
346
  const id = requireMeta(cwd)
323
347
  const detail = await apiFetch(`/api/abstracts/${id}`)
348
+ warnIfRemoteChanged(detail)
324
349
  const records = await apiFetch(`/api/abstracts/${id}/records`)
325
350
  const rows = records.records || []
326
351
  const shared = rows.filter((r) => r.visibility !== 'private')
@@ -341,6 +366,7 @@ async function cmdPush(cwd, rest) {
341
366
  const { flags } = parseFlags(rest)
342
367
  if (flags.help) return printHelp('push')
343
368
  const id = requireMeta(cwd)
369
+ warnIfRemoteChanged(await apiFetch(`/api/abstracts/${id}`))
344
370
  if (flags.data) {
345
371
  const localRecords = recordsFromLocal(cwd)
346
372
  const { refused } = await apiFetch(`/api/abstracts/${id}/records`, {
@@ -373,8 +399,27 @@ async function cmdServe(cwd, rest) {
373
399
  port,
374
400
  json: jsonMode(flags),
375
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)
376
420
  await new Promise((resolve) => {
377
421
  const stop = () => {
422
+ clearInterval(remoteTimer)
378
423
  running.close().then(resolve, resolve)
379
424
  }
380
425
  process.once('SIGINT', stop)
@@ -403,8 +448,10 @@ async function cmdPreview(cwd, rest) {
403
448
  const { flags, positional } = parseFlags(rest)
404
449
  if (flags.help) return printHelp('preview')
405
450
  const id = await resolveAbstractId(cwd, flags, positional)
451
+ const detail = await apiFetch(`/api/abstracts/${id}`)
452
+ warnIfRemoteChanged(detail)
406
453
  const urls = urlsFor(id)
407
- if (jsonMode(flags)) printJson({ preview: urls.preview })
454
+ if (jsonMode(flags)) printJson({ preview: urls.preview, remoteChangedSincePush: Boolean(detail.remoteChangedSincePush) })
408
455
  else console.log(urls.preview)
409
456
  }
410
457
 
@@ -412,6 +459,7 @@ async function cmdPublish(cwd, rest) {
412
459
  const { flags, positional } = parseFlags(rest)
413
460
  if (flags.help) return printHelp('publish')
414
461
  const id = await resolveAbstractId(cwd, flags, positional)
462
+ warnIfRemoteChanged(await apiFetch(`/api/abstracts/${id}`))
415
463
  const live = Boolean(flags.live)
416
464
  const version = await apiFetch(`/api/abstracts/${id}/publish`, {
417
465
  method: 'POST',
@@ -761,6 +809,8 @@ export async function runCommand(argv, cwd = process.cwd()) {
761
809
  return cmdCreate(cwd, rest)
762
810
  case 'invite':
763
811
  return cmdInvite(cwd, rest)
812
+ case 'notify':
813
+ return cmdNotify(cwd, rest)
764
814
  case 'remove':
765
815
  return cmdRemove(cwd, rest)
766
816
  case 'people':
package/lib/help.mjs CHANGED
@@ -13,7 +13,8 @@ export const ROOT_HELP = `Amorphous
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
@@ -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'
@@ -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.11",
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",
@@ -52,7 +52,8 @@ When they ask:
52
52
  - `amorphous-cli push` sends the source. `amorphous-cli push --data` also sends the records in this folder.
53
53
  - `amorphous-cli preview` and open that URL.
54
54
  - `amorphous-cli publish --live`
55
- - `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.
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.
56
57
 
57
58
  `amorphous-cli visibility anyone` lets anyone view and write shared records. `amorphous-cli visibility members` keeps the abstract to invited people.
58
59