amorphous-cli 0.3.3 → 0.3.4
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-guide-commands.test.mjs +40 -0
- package/lib/commands.mjs +38 -7
- package/lib/folder-command.test.mjs +178 -0
- package/lib/help.mjs +13 -4
- package/lib/help.test.mjs +22 -1
- package/package.json +1 -1
- package/skill/amorphous/SKILL.md +4 -0
|
@@ -107,6 +107,46 @@ test('context fetches only authorized guide endpoints and materializes both docs
|
|
|
107
107
|
assert.equal(calls.length, 4, 'a new context invocation freshly authorizes both guides')
|
|
108
108
|
})
|
|
109
109
|
|
|
110
|
+
test('context does not ask the agent to read guides that contain no text', async (t) => {
|
|
111
|
+
const h = await contextHarness(t, {
|
|
112
|
+
responder(req, res) {
|
|
113
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
114
|
+
res.end(JSON.stringify({ body: '', revision: 0, updatedAt: null, updatedBy: null }))
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
const logs = []
|
|
118
|
+
const originalLog = console.log
|
|
119
|
+
console.log = (...args) => logs.push(args.join(' '))
|
|
120
|
+
try {
|
|
121
|
+
await runCommand(['context'], h.project)
|
|
122
|
+
} finally { console.log = originalLog }
|
|
123
|
+
|
|
124
|
+
assert.equal(readFileSync(join(h.project, 'agent.md'), 'utf8'), '')
|
|
125
|
+
assert.equal(readFileSync(join(h.project, 'private-agent.md'), 'utf8'), '')
|
|
126
|
+
assert.ok(logs.includes('Neither guide contains text; continue with the abstract task.'))
|
|
127
|
+
assert.equal(logs.some((line) => line.startsWith('Read agent.md')), false)
|
|
128
|
+
assert.equal(logs.some((line) => line.startsWith('The private guide is supplied')), false)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('context names only guide files that contain text', async (t) => {
|
|
132
|
+
const h = await contextHarness(t, {
|
|
133
|
+
responder(req, res) {
|
|
134
|
+
const isPrivate = req.url.endsWith('/private-agent-guide')
|
|
135
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
136
|
+
res.end(JSON.stringify({ body: isPrivate ? '' : 'Shared instructions.', revision: isPrivate ? 0 : 1, updatedAt: null, updatedBy: null }))
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
const logs = []
|
|
140
|
+
const originalLog = console.log
|
|
141
|
+
console.log = (...args) => logs.push(args.join(' '))
|
|
142
|
+
try {
|
|
143
|
+
await runCommand(['context'], h.project)
|
|
144
|
+
} finally { console.log = originalLog }
|
|
145
|
+
|
|
146
|
+
assert.ok(logs.includes('Read agent.md before working on the abstract.'))
|
|
147
|
+
assert.equal(logs.some((line) => line.includes('private-agent.md before')), false)
|
|
148
|
+
})
|
|
149
|
+
|
|
110
150
|
test('context refuses both guides above the combined cap before writing either', async (t) => {
|
|
111
151
|
const root = mkdtempSync(join(tmpdir(), 'am-guide-cap-'))
|
|
112
152
|
const project = join(root, 'project')
|
package/lib/commands.mjs
CHANGED
|
@@ -227,19 +227,25 @@ function quarantineActiveGuides(cwd, fallbackAbstractId) {
|
|
|
227
227
|
|
|
228
228
|
function enforceActiveGuideCaps(cwd, abstractId, userId) {
|
|
229
229
|
const paths = guidePaths(cwd, abstractId, userId)
|
|
230
|
-
const files = [
|
|
230
|
+
const files = [
|
|
231
|
+
{ name: 'agent.md', path: paths.abstractGuide },
|
|
232
|
+
{ name: 'private-agent.md', path: paths.privateGuide },
|
|
233
|
+
]
|
|
231
234
|
let combinedBytes = 0
|
|
232
|
-
|
|
235
|
+
const inspected = []
|
|
236
|
+
for (const { path } of files) {
|
|
233
237
|
const info = lstatSync(path)
|
|
234
238
|
if (info.isSymbolicLink() || !info.isFile()) throw new Error('Active guide files must be regular non-symlink files.')
|
|
235
239
|
if (info.size > GUIDE_MAX_BYTES) {
|
|
236
240
|
throw new Error('An active guide exceeds the 32 KiB UTF-8 context limit. Neither guide was loaded; local text is preserved without truncation.')
|
|
237
241
|
}
|
|
238
242
|
combinedBytes += info.size
|
|
243
|
+
inspected.push(info.size)
|
|
239
244
|
}
|
|
240
245
|
if (combinedBytes > COMBINED_GUIDE_MAX_BYTES) {
|
|
241
246
|
throw new Error('Active guide files exceed the combined 48 KiB UTF-8 context limit. Neither guide was loaded; local text is preserved without truncation.')
|
|
242
247
|
}
|
|
248
|
+
return files.filter((_, index) => inspected[index] > 0).map(({ name }) => name)
|
|
243
249
|
}
|
|
244
250
|
|
|
245
251
|
async function cmdContext(cwd, rest) {
|
|
@@ -277,7 +283,7 @@ async function cmdContext(cwd, rest) {
|
|
|
277
283
|
}
|
|
278
284
|
outcomes.push(outcome)
|
|
279
285
|
}
|
|
280
|
-
enforceActiveGuideCaps(cwd, id, userId)
|
|
286
|
+
const nonemptyGuides = enforceActiveGuideCaps(cwd, id, userId)
|
|
281
287
|
const conflicts = outcomes.filter((outcome) => outcome.status === 'conflict')
|
|
282
288
|
if (conflicts.length) {
|
|
283
289
|
const paths = conflicts.map((item) => item.conflictPath).join(', ')
|
|
@@ -285,8 +291,14 @@ async function cmdContext(cwd, rest) {
|
|
|
285
291
|
}
|
|
286
292
|
console.log(`Loaded guide context for ${id} (abstract revision ${abstractGuide.revision}; private revision ${privateGuide.revision}).`)
|
|
287
293
|
console.log('Context does not fetch source; source release/version is separate from these guide revisions.')
|
|
288
|
-
|
|
289
|
-
|
|
294
|
+
if (nonemptyGuides.includes('private-agent.md')) {
|
|
295
|
+
console.log('The private guide is supplied to this local agent process and cannot be recalled after download.')
|
|
296
|
+
}
|
|
297
|
+
if (nonemptyGuides.length) {
|
|
298
|
+
console.log(`Read ${nonemptyGuides.join(' and ')} before working on the abstract.`)
|
|
299
|
+
} else {
|
|
300
|
+
console.log('Neither guide contains text; continue with the abstract task.')
|
|
301
|
+
}
|
|
290
302
|
} catch (error) {
|
|
291
303
|
if (!authorizationComplete) {
|
|
292
304
|
try { quarantineActiveGuides(cwd, id) } catch (quarantineError) {
|
|
@@ -988,14 +1000,33 @@ async function cmdFolderMove(rest) {
|
|
|
988
1000
|
console.log('Moved folder.')
|
|
989
1001
|
}
|
|
990
1002
|
|
|
1003
|
+
async function cmdFolderRemove(rest) {
|
|
1004
|
+
const { flags, positional } = parseFlags(rest)
|
|
1005
|
+
const token = flags.id || positional[0]
|
|
1006
|
+
if (!token) throw new Error('Pass a folder name or id.')
|
|
1007
|
+
const folders = await loadFolders()
|
|
1008
|
+
const want = String(token)
|
|
1009
|
+
const folder = folders.find((candidate) => candidate.id === want || candidate.path === want)
|
|
1010
|
+
const nameMatches = folders.filter((candidate) => candidate.name.toLowerCase() === want.toLowerCase())
|
|
1011
|
+
if (!folder && nameMatches.length > 1) {
|
|
1012
|
+
throw new Error(`More than one folder is named “${token}”. Use its full path or id.`)
|
|
1013
|
+
}
|
|
1014
|
+
const target = folder || nameMatches[0]
|
|
1015
|
+
if (!target) throw new Error(`No folder named “${token}”`)
|
|
1016
|
+
await apiFetch(`/api/organise/collections/${target.id}`, { method: 'DELETE' })
|
|
1017
|
+
if (jsonMode(flags)) printJson({ id: target.id, name: target.name, removed: true })
|
|
1018
|
+
else console.log('Folder removed.')
|
|
1019
|
+
}
|
|
1020
|
+
|
|
991
1021
|
async function cmdFolder(rest) {
|
|
992
1022
|
const { flags } = parseFlags(rest)
|
|
993
|
-
if (flags.help || !rest[0]) return printHelp('folder')
|
|
1023
|
+
if (flags.help || !rest[0]) return printHelp(rest[0] === 'list' ? 'folder-list' : 'folder')
|
|
994
1024
|
const [sub, ...tail] = rest
|
|
995
1025
|
if (sub === 'new') return cmdFolderNew(tail)
|
|
996
1026
|
if (sub === 'list') return cmdFolderList(tail)
|
|
997
1027
|
if (sub === 'move') return cmdFolderMove(tail)
|
|
998
|
-
|
|
1028
|
+
if (sub === 'remove') return cmdFolderRemove(tail)
|
|
1029
|
+
throw new Error('folder needs new, list, move, or remove.')
|
|
999
1030
|
}
|
|
1000
1031
|
|
|
1001
1032
|
export async function runCommand(argv, cwd = process.cwd()) {
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { createServer } from 'node:http'
|
|
3
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import test from 'node:test'
|
|
7
|
+
import { runCommand } from './commands.mjs'
|
|
8
|
+
import { saveSession } from './session.mjs'
|
|
9
|
+
|
|
10
|
+
const folderId = '00000000-0000-4000-8000-000000000003'
|
|
11
|
+
const userId = '10000000-0000-4000-8000-000000000001'
|
|
12
|
+
const payload = (value) => Buffer.from(JSON.stringify(value)).toString('base64url')
|
|
13
|
+
const accessToken = `header.${payload({ sub: userId, exp: Math.floor(Date.now() / 1000) + 3600 })}.signature`
|
|
14
|
+
|
|
15
|
+
async function folderHarness(t, deleteStatus = 200, collections) {
|
|
16
|
+
const root = mkdtempSync(join(tmpdir(), 'am-folder-command-'))
|
|
17
|
+
const oldCredentials = process.env.AMORPHOUS_CREDENTIALS
|
|
18
|
+
const oldAppUrl = process.env.AMORPHOUS_CLI_APP_URL
|
|
19
|
+
const oldAppOrigin = process.env.APP_ORIGIN
|
|
20
|
+
process.env.AMORPHOUS_CREDENTIALS = join(root, 'credentials.json')
|
|
21
|
+
process.env.APP_ORIGIN = 'https://amorphous.test'
|
|
22
|
+
saveSession({
|
|
23
|
+
access_token: accessToken,
|
|
24
|
+
refresh_token: 'unused',
|
|
25
|
+
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const calls = []
|
|
29
|
+
const server = createServer((req, res) => {
|
|
30
|
+
calls.push({ method: req.method, url: req.url, authorization: req.headers.authorization })
|
|
31
|
+
res.setHeader('content-type', 'application/json')
|
|
32
|
+
if (req.method === 'GET' && req.url === '/api/organise/meta') {
|
|
33
|
+
res.end(JSON.stringify({
|
|
34
|
+
all_collections: collections || [{
|
|
35
|
+
id: folderId,
|
|
36
|
+
name: 'QA smoke 2026-10-21',
|
|
37
|
+
path: 'QA smoke 2026-10-21',
|
|
38
|
+
parent_id: null,
|
|
39
|
+
abstract_count: 0,
|
|
40
|
+
}],
|
|
41
|
+
}))
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
if (req.method === 'DELETE' && req.url === `/api/organise/collections/${folderId}`) {
|
|
45
|
+
res.statusCode = deleteStatus
|
|
46
|
+
res.end(JSON.stringify(deleteStatus === 200 ? { ok: true } : {
|
|
47
|
+
statusCode: 409,
|
|
48
|
+
statusMessage: 'Move the folder contents out before removing it',
|
|
49
|
+
message: 'Move the folder contents out before removing it',
|
|
50
|
+
}))
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
res.statusCode = 404
|
|
54
|
+
res.end(JSON.stringify({ message: 'Not found' }))
|
|
55
|
+
})
|
|
56
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
57
|
+
process.env.AMORPHOUS_CLI_APP_URL = `http://127.0.0.1:${server.address().port}`
|
|
58
|
+
|
|
59
|
+
t.after(async () => {
|
|
60
|
+
server.closeAllConnections?.()
|
|
61
|
+
await new Promise((resolve) => server.close(resolve))
|
|
62
|
+
rmSync(root, { recursive: true, force: true })
|
|
63
|
+
if (oldCredentials === undefined) delete process.env.AMORPHOUS_CREDENTIALS
|
|
64
|
+
else process.env.AMORPHOUS_CREDENTIALS = oldCredentials
|
|
65
|
+
if (oldAppUrl === undefined) delete process.env.AMORPHOUS_CLI_APP_URL
|
|
66
|
+
else process.env.AMORPHOUS_CLI_APP_URL = oldAppUrl
|
|
67
|
+
if (oldAppOrigin === undefined) delete process.env.APP_ORIGIN
|
|
68
|
+
else process.env.APP_ORIGIN = oldAppOrigin
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return calls
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test('folder remove resolves a folder name and deletes the empty folder', async (t) => {
|
|
75
|
+
const calls = await folderHarness(t)
|
|
76
|
+
let output = ''
|
|
77
|
+
const originalLog = console.log
|
|
78
|
+
console.log = (message) => { output = String(message) }
|
|
79
|
+
try {
|
|
80
|
+
await runCommand(['folder', 'remove', 'QA smoke 2026-10-21'])
|
|
81
|
+
} finally {
|
|
82
|
+
console.log = originalLog
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
assert.deepEqual(calls.map(({ method, url }) => [method, url]), [
|
|
86
|
+
['GET', '/api/organise/meta'],
|
|
87
|
+
['DELETE', `/api/organise/collections/${folderId}`],
|
|
88
|
+
])
|
|
89
|
+
assert.ok(calls.every((call) => call.authorization === `Bearer ${accessToken}`))
|
|
90
|
+
assert.equal(output, 'Folder removed.')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('folder remove requires an id or path when folder names collide', async (t) => {
|
|
94
|
+
const calls = await folderHarness(t, 200, [
|
|
95
|
+
{
|
|
96
|
+
id: folderId,
|
|
97
|
+
name: 'QA smoke',
|
|
98
|
+
path: 'Production / QA smoke',
|
|
99
|
+
parent_id: '00000000-0000-4000-8000-000000000005',
|
|
100
|
+
abstract_count: 0,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: '00000000-0000-4000-8000-000000000004',
|
|
104
|
+
name: 'QA smoke',
|
|
105
|
+
path: 'Launch / QA smoke',
|
|
106
|
+
parent_id: '00000000-0000-4000-8000-000000000005',
|
|
107
|
+
abstract_count: 0,
|
|
108
|
+
},
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
await assert.rejects(
|
|
112
|
+
runCommand(['folder', 'remove', 'QA smoke']),
|
|
113
|
+
/More than one folder is named.*full path or id/,
|
|
114
|
+
)
|
|
115
|
+
assert.deepEqual(calls.map(({ method, url }) => [method, url]), [
|
|
116
|
+
['GET', '/api/organise/meta'],
|
|
117
|
+
])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('folder remove reports that a non-empty folder must be cleared first', async (t) => {
|
|
121
|
+
const calls = await folderHarness(t, 409)
|
|
122
|
+
await assert.rejects(
|
|
123
|
+
runCommand(['folder', 'remove', folderId]),
|
|
124
|
+
/Move the folder contents out before removing it/,
|
|
125
|
+
)
|
|
126
|
+
assert.deepEqual(calls.map(({ method, url }) => [method, url]), [
|
|
127
|
+
['GET', '/api/organise/meta'],
|
|
128
|
+
['DELETE', `/api/organise/collections/${folderId}`],
|
|
129
|
+
])
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('folder remove returns machine-readable confirmation in JSON mode', async (t) => {
|
|
133
|
+
await folderHarness(t)
|
|
134
|
+
let output = ''
|
|
135
|
+
const originalLog = console.log
|
|
136
|
+
console.log = (message) => { output = String(message) }
|
|
137
|
+
try {
|
|
138
|
+
await runCommand(['folder', 'remove', folderId, '--json'])
|
|
139
|
+
} finally {
|
|
140
|
+
console.log = originalLog
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
assert.deepEqual(JSON.parse(output), {
|
|
144
|
+
id: folderId,
|
|
145
|
+
name: 'QA smoke 2026-10-21',
|
|
146
|
+
removed: true,
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('folder help describes the remove command and its empty-folder requirement', async () => {
|
|
151
|
+
let output = ''
|
|
152
|
+
const originalLog = console.log
|
|
153
|
+
console.log = (message) => { output = String(message) }
|
|
154
|
+
try {
|
|
155
|
+
await runCommand(['folder', '--help'])
|
|
156
|
+
} finally {
|
|
157
|
+
console.log = originalLog
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
assert.match(output, /folder remove <folder>/)
|
|
161
|
+
assert.match(output, /Only empty folders can be removed/i)
|
|
162
|
+
assert.match(output, /--json/)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('folder list help stays focused on listing folders', async () => {
|
|
166
|
+
let output = ''
|
|
167
|
+
const originalLog = console.log
|
|
168
|
+
console.log = (message) => { output = String(message) }
|
|
169
|
+
try {
|
|
170
|
+
await runCommand(['folder', 'list', '--help'])
|
|
171
|
+
} finally {
|
|
172
|
+
console.log = originalLog
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
assert.match(output, /folder list \[--json\]/)
|
|
176
|
+
assert.match(output, /list your folders/i)
|
|
177
|
+
assert.doesNotMatch(output, /folder (new|move|remove)/)
|
|
178
|
+
})
|
package/lib/help.mjs
CHANGED
|
@@ -15,7 +15,7 @@ export const ROOT_HELP = `Amorphous
|
|
|
15
15
|
publish [--live] Publish a version; --live is Go live
|
|
16
16
|
recommend [--note <text>] Send this draft to the owner for review
|
|
17
17
|
live [abstract] Make a version live
|
|
18
|
-
visibility anyone|members Anyone can view / Members only
|
|
18
|
+
visibility anyone|members Anyone can view; app may accept shared-record writes / Members only
|
|
19
19
|
invite <email> Email an invite and print the link
|
|
20
20
|
notify Email members about the published version
|
|
21
21
|
remove <email|id> Remove
|
|
@@ -28,6 +28,7 @@ export const ROOT_HELP = `Amorphous
|
|
|
28
28
|
folder new New folder
|
|
29
29
|
folder list Folders
|
|
30
30
|
folder move Move folder
|
|
31
|
+
folder remove Remove empty folder
|
|
31
32
|
rename --name Rename
|
|
32
33
|
|
|
33
34
|
Run amorphous-cli <command> --help for flags. Pass --json where you need machine output.
|
|
@@ -105,7 +106,9 @@ Make this the live version. Uses the latest published version if you omit --vers
|
|
|
105
106
|
|
|
106
107
|
visibility: `visibility <anyone|members> [abstract] [--version <id>]
|
|
107
108
|
|
|
108
|
-
Anyone can view
|
|
109
|
+
Anyone: anyone with the link can view. If the app writes shared records, anyone with the link can add them; use this only when public submissions are intended.
|
|
110
|
+
Members: only invited people can view; shared-record writes follow their access.
|
|
111
|
+
Uses the live version if you omit --version.`,
|
|
109
112
|
|
|
110
113
|
invite: `invite <email> [view|use|edit] [--abstract <id|name>] [--json]
|
|
111
114
|
|
|
@@ -155,12 +158,18 @@ Unstar.`,
|
|
|
155
158
|
|
|
156
159
|
Suggest folder from the name. Default is a suggestion only. --apply uses it.`,
|
|
157
160
|
|
|
158
|
-
folder: `folder <new|list|move> …
|
|
161
|
+
folder: `folder <new|list|move|remove> …
|
|
159
162
|
|
|
160
163
|
folder new --name <name> [--parent <id>]
|
|
161
164
|
folder list [--json]
|
|
162
165
|
folder move <folder> --parent <id|none>
|
|
163
|
-
|
|
166
|
+
folder remove <folder> [--json]
|
|
167
|
+
New folder, or move a folder. Folders organize your library and do not grant access.
|
|
168
|
+
Only empty folders can be removed; move their contents out first.`,
|
|
169
|
+
|
|
170
|
+
'folder-list': `folder list [--json]
|
|
171
|
+
|
|
172
|
+
List your folders.`,
|
|
164
173
|
|
|
165
174
|
rename: `rename [abstract] --name <name>
|
|
166
175
|
|
package/lib/help.test.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import test from 'node:test'
|
|
3
|
-
import { printHelp } from './help.mjs'
|
|
3
|
+
import { printHelp, ROOT_HELP } from './help.mjs'
|
|
4
4
|
|
|
5
5
|
test('serve help explains that prod mode can write live records', () => {
|
|
6
6
|
let output = ''
|
|
@@ -33,3 +33,24 @@ test('pull help makes the fresh-folder abstract target explicit', () => {
|
|
|
33
33
|
assert.match(output, /before context/i)
|
|
34
34
|
assert.match(output, /owner or editor/i)
|
|
35
35
|
})
|
|
36
|
+
|
|
37
|
+
test('visibility help explains public shared-record writes', () => {
|
|
38
|
+
let output = ''
|
|
39
|
+
const originalLog = console.log
|
|
40
|
+
console.log = (message) => { output = String(message) }
|
|
41
|
+
try {
|
|
42
|
+
printHelp('visibility')
|
|
43
|
+
} finally {
|
|
44
|
+
console.log = originalLog
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
assert.match(output, /if the app writes shared records/i)
|
|
48
|
+
assert.match(output, /anyone with the link can add them/i)
|
|
49
|
+
assert.match(output, /members: only invited people can view/i)
|
|
50
|
+
assert.match(output, /shared-record writes follow their access/i)
|
|
51
|
+
assert.match(output, /live version/i)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('root help does not hide the write effect of public visibility', () => {
|
|
55
|
+
assert.match(ROOT_HELP, /anyone can view; app may accept shared-record writes/i)
|
|
56
|
+
})
|
package/package.json
CHANGED
package/skill/amorphous/SKILL.md
CHANGED
|
@@ -50,6 +50,8 @@ Then choose the path that matches the request:
|
|
|
50
50
|
- **Access:** View members can open the live abstract and load its guides, but cannot pull, recommend, or change source. Use members can use the live abstract and submit source recommendations for the owner to review when source files are already available to them. Only owners and editors can pull and edit source or publish a version; only the owner can invite/remove members, change visibility, or make a version live. Follow the access already granted; ask the owner for a role change if the requested work needs more access.
|
|
51
51
|
- **New abstract:** Run `amorphous-cli create --name "..." [--template blank|list|board|landing]`. The list, board, and landing starters are working examples, not just mockups.
|
|
52
52
|
|
|
53
|
+
After `context` succeeds, follow its final guide-reading instruction. It names only guide files that contain text; if neither guide contains text, continue without opening empty guide files. The private-guide handling warning appears only when that guide contains text.
|
|
54
|
+
|
|
53
55
|
Edit the files to match what they asked for. `me` and `names` are how the abstract knows who is here. Prefer `amorphous-cli serve` while you edit. Open that URL and leave it running; it reloads when the files change, making it the easiest way to check the work. Records start local in this folder. Use prod mode only when the person explicitly asks you to inspect or change live records: `find` reads live data, while `insert` and `replace` change it immediately. Do not write live records during UI work or read-only checks. You can push and preview without prod mode.
|
|
54
56
|
|
|
55
57
|
## Loop
|
|
@@ -64,6 +66,8 @@ When they ask:
|
|
|
64
66
|
- `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.
|
|
65
67
|
- `amorphous-cli notify` emails members that the published version changed. It does not publish. Do that only when they ask.
|
|
66
68
|
|
|
69
|
+
Use `amorphous-cli folder list` to inspect library folders, `amorphous-cli folder new --name "<name>"` to create one, and `amorphous-cli folder move <folder> --parent <id|none>` to reorganize them. Remove an empty folder with `amorphous-cli folder remove <folder>`; move its abstracts and subfolders out first.
|
|
70
|
+
|
|
67
71
|
`amorphous-cli visibility anyone` lets anyone view the abstract and write shared records through it. Those writes persist on the abstract. Make it public only when the person explicitly asks for public access and understands that anyone can add shared records. `amorphous-cli visibility members` keeps the abstract to invited people.
|
|
68
72
|
|
|
69
73
|
Do not make a public abstract that lets anyone keep adding shared records, such as an open list. Those records stay on the abstract and use up its storage. Keep shared records to a set you replace, or keep the abstract to invited people.
|