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.
- package/lib/agent-contract.mjs +1 -1
- package/lib/api.mjs +6 -2
- package/lib/commands.mjs +63 -4
- package/lib/help.mjs +9 -4
- package/mcp.mjs +15 -3
- package/package.json +1 -1
- package/skill/amorphous/SKILL.md +17 -5
package/lib/agent-contract.mjs
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
270
|
-
|
|
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
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
'
|
|
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
package/skill/amorphous/SKILL.md
CHANGED
|
@@ -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`
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|