@heyquilt/seam 0.143.0 → 0.145.0

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/bin/seam.js CHANGED
@@ -21,9 +21,11 @@ Docs: https://seam.heyquilt.com/docs
21
21
  seam logout | seam whoami
22
22
  seam computers [list | signout <id or name>] computers signed in to your account (seam login)
23
23
  seam handle [<new>] your name in your repos' addresses (<handle>/<repo>); with <new>, change it
24
- seam account delete --confirm <your email> delete your Seam account (your patches stay)
24
+ seam account delete --confirm <your email> delete your Seam account (your patches stay; repos you deleted are purged)
25
+ seam account log [-n <count>] [--before <id>] what happened to your repos: deleted, restored, purged
25
26
  seam agent new <name> | list | key <name> | revoke <name> | relink [agent:<id>] your agents and their keys
26
27
  seam oauth client new <name> --redirect <uri> | list | revoke <client_id> apps that ask people for access (OAuth 2, PKCE)
28
+ seam oauth allowed [list | revoke <grant id>] the apps you allowed into your account, and ending one
27
29
  seam quilts [new <slug> | show <q>] Your quilts (repositories)
28
30
  seam import git <path> [--into <slug>] [--authors <file>]
29
31
  seam github [import <owner/name> --installation <id> [--into <slug>]] from GitHub, followed
@@ -98,6 +100,9 @@ Docs: https://seam.heyquilt.com/docs
98
100
  seam org log <org> [-n <count>] [--before <id>] what happened in an org: members, roles, teams, quilts (owners and admins)
99
101
  seam org team <org> <name> [<member>...] an org's team, named org_team:<name> in its quilts' protections
100
102
  seam quilts new <slug> --org <org> | quilts move --quilt <q> --org <org> a quilt that belongs to an org
103
+ seam quilts rename <new-slug> --quilt <q> [--title <name>] a new address (<owner>/<new-slug>); the old one stays an alias
104
+ seam quilts delete <q> --confirm <owner/name> delete a repo (its owner, or an org's admin); restorable for 7 days, then purged
105
+ seam quilts deleted [--org <org>] | restore <slug> [--org <org>] Recently deleted, and bringing one back as it was
101
106
  seam quilts agents-make-lines on|off whether agents may make lines, tags and releases
102
107
  seam quilts public on|off anyone may read the quilt, signed in or not
103
108
  seam quilts add <email|member> [--level read|write|admin] add a member; an email with no account yet is invited
@@ -139,7 +144,7 @@ async function main () {
139
144
  const r = await go('PUT', '/v1/me/handle', { handle: argv[0] })
140
145
  return console.log(`Your handle is ${r.handle}: your repos are ${r.handle}/<repo> now, and their old addresses stop working.`)
141
146
  }
142
- if (cmd === 'account') {
147
+ if (cmd === 'account' && argv[0] !== 'log') {
143
148
  const i = argv.indexOf('--confirm')
144
149
  if (argv[0] !== 'delete' || i < 0 || !argv[i + 1]) fail('seam account delete --confirm <your email>')
145
150
  const r = await signedIn()('DELETE', `/v1/me?confirm=${encodeURIComponent(argv[i + 1])}`)
@@ -185,7 +190,16 @@ async function oauthCommand (argv) {
185
190
  }
186
191
  }
187
192
  const [what, sub, ...rest] = argv
188
- if (what !== 'client') fail('seam oauth client new <name> --redirect <uri> | list | revoke <client_id>')
193
+ // The apps you allowed (GET /v1/me/grants): what each may do; revoking one ends its tokens.
194
+ if (what === 'allowed') {
195
+ if (sub === 'revoke') { if (!rest[0]) fail('seam oauth allowed revoke <grant id>'); await go('DELETE', `/v1/me/grants/${encodeURIComponent(rest[0])}`); return console.log(`Revoked: the app's tokens from that approval have ended. It asks you again to come back.`) }
196
+ if (sub && sub !== 'list') fail(`seam oauth allowed: '${sub}' is not a command (list, revoke).`)
197
+ const { grants } = await go('GET', '/v1/me/grants')
198
+ if (!grants.length) return console.log('You have allowed no apps.')
199
+ for (const g of grants) console.log(`${g.id} ${g.client.name} ${g.scopes.map((x) => { const q = g.quilts.find((y) => x === `quilt:${y.id}:${y.level}`); return q ? `${q.slug || q.id}:${q.level}` : x }).join(' ')}`)
200
+ return
201
+ }
202
+ if (what !== 'client') fail('seam oauth client new <name> --redirect <uri> | list | revoke <client_id> | seam oauth allowed [list | revoke <grant id>]')
189
203
  if (sub === 'new') {
190
204
  const redirects = []; const words = []
191
205
  for (let i = 0; i < rest.length; i++) { if (rest[i] === '--redirect') redirects.push(rest[++i]); else words.push(rest[i]) }
@@ -892,10 +892,22 @@ export async function pull (client, dir, block) {
892
892
  const s = await toLineTip(client, dir, block, sq.line, tip)
893
893
  return { left: { square: short, state: sq.state }, line: sq.line, at: tip, written: s.written, merged: s.merged, conflicted: s.conflicted }
894
894
  }
895
+ // The square moved on the server since this folder last looked (the site's Merge brought its
896
+ // line in, conflicts kept in its new head; or a commit from another folder): the folder takes
897
+ // that head first, markers and all, as seam checkout would. Uncommitted edits stay (syncTo).
898
+ const caught = (sq.head || null) !== (block.head || null) ? { from: block.head || null, ...await syncTo(client, dir, block, sq.head || sq.base, { square: sq }) } : null
895
899
  const r = await client.rebase(block.quilt, block.square, { expect_head: block.head })
896
- if (r.noop) return { rebased: false, noop: true, square: r.square }
897
- const s = await syncTo(client, dir, block, r.square.head || r.square.base, { square: r.square })
898
- return { rebased: true, square: r.square, result: r, written: s.written, merged: s.merged, conflicted: s.conflicted }
900
+ if (r.noop && !caught) return { rebased: false, noop: true, square: r.square }
901
+ const s = r.noop ? { written: 0, merged: [], conflicted: [] } : await syncTo(client, dir, block, r.square.head || r.square.base, { square: r.square })
902
+ const both = (k) => [...new Set([...(caught?.[k] || []), ...s[k]])].sort(cmpPath)
903
+ const conflicted = both('conflicted')
904
+ // The conflicts the square itself holds (terms in its head), apart from the folder's own clashes.
905
+ const held = Object.entries(block.files).filter(([p, f]) => f.term && !conflicted.includes(p)).map(([p]) => p).sort(cmpPath)
906
+ return {
907
+ rebased: !r.noop, square: r.square, ...(r.noop ? {} : { result: r }),
908
+ ...(caught ? { caught_up: { from: caught.from, to: sq.head || sq.base } } : {}),
909
+ written: (caught?.written || 0) + s.written, merged: both('merged'), conflicted, held
910
+ }
899
911
  }
900
912
  if (!isLineCheckout(block)) throw new Error('This folder holds one revision, not a line: seam checkout <line or square> moves it.')
901
913
  const to = (await client.show(block.quilt, block.line, { format: 'none' })).commit.id
package/lib/quilts/cli.js CHANGED
@@ -9,7 +9,7 @@ import { quiltClient } from './client.js'
9
9
  import { runChecks, readRunnerConfig, listenForWork } from './runner.js'
10
10
  import { startDaemon, askDaemon } from './daemon.js'
11
11
  import { rebuild as rebuildBlock, conflictStyle } from './block.js'
12
- import { describeRebuild, describeOpened, describeBoth } from './describe.js'
12
+ import { describeRebuild, describeOpened, describeBoth, describePull, describeDeletedQuilt, describeDeletedQuilts, describePersonLog } from './describe.js'
13
13
  import { findBlock, checkout, status as blockStatus, commitFolder, amendFolder, syncTo, localDiff, saveBlock, pull, toLineTip, restore, reset, clean, localChanges, bytesOf, livePatch, resolvePulled } from './block.js'
14
14
  import { describeLog, describeShow, describeDiff, describeBlame, describeStatus, describeMerge, describeWrite, describeSquare, describeSquareStatus, describeEvents, describeTag, describeRelease, describeImport, describeImportRecord, describeRestore, describeReset, describeClean, describeLive, describeClaims, describeHandoff, describeFinished, describeResolutions, describeLiveSet, describeLiveJoin, describeMergeBase, describeStack, describeStackDiff, describeGrep, describeBisect, describeCheck, describeReviews, describeInterdiff, describeReviewed, describeSquareCheck, describePolicy, describeKeys, describeTokens, describeNewToken, describeBlock, describeBlocks, describeProtections, describeTeams, describeAccess, describeRedacted, describeRedactions, describeMetrics, describeSquarePolicy, describePolicyVersions, describeOrg, describeOrgs, describeOrgLog, describeOrgDomain, describeApp, describeApps, describeAppKey, describeInstall, describeInstalls, describeRequest, describeRequests, describeOrgTeams, describeCheckRuns, describeRerun, describeAncestor, describeContains, describePipeline, describeWatches, describeSearch, describePublicReads } from './describe.js'
15
15
  import { verifyQuilt, describeVerify } from './attest.js'
@@ -21,7 +21,7 @@ import { signWithLocalKey, localKey, makeLocalKey, signingKeyFile } from './sign
21
21
  export const API_URL = 'https://api.seam.heyquilt.com'
22
22
 
23
23
  /** The verbs `seam` answers. */
24
- export const SEAM_VERBS = new Set(['quilts', 'square', 'checkout', 'status', 'commit', 'log', 'show', 'diff', 'merge', 'rebase', 'revert', 'cherry-pick', 'blame', 'resolve', 'cat', 'ls', 'import', 'export', 'events', 'tag', 'release', 'line', 'restore', 'reset', 'clean', 'live', 'bisect', 'grep', 'describe', 'merge-base', 'stack', 'claim', 'unclaim', 'check', 'checks', 'resolutions', 'review', 'sign', 'key', 'token', 'block', 'runner', 'protect', 'team', 'verify', 'redact', 'metrics', 'seal', 'policy', 'org', 'app', 'watch', 'search', 'contains', 'daemon', 'clone', 'pull'])
24
+ export const SEAM_VERBS = new Set(['quilts', 'square', 'checkout', 'status', 'commit', 'log', 'show', 'diff', 'merge', 'rebase', 'revert', 'cherry-pick', 'blame', 'resolve', 'cat', 'ls', 'import', 'export', 'events', 'tag', 'release', 'line', 'restore', 'reset', 'clean', 'live', 'bisect', 'grep', 'describe', 'merge-base', 'stack', 'claim', 'unclaim', 'check', 'checks', 'resolutions', 'review', 'sign', 'key', 'token', 'block', 'runner', 'protect', 'team', 'verify', 'redact', 'metrics', 'seal', 'policy', 'org', 'app', 'watch', 'search', 'contains', 'daemon', 'clone', 'pull', 'account'])
25
25
 
26
26
  /** `ci=agent:ci,lint=agent:a+agent:b` → { ci: ['agent:ci'], lint: ['agent:a', 'agent:b'] }; `none` clears them. */
27
27
  export function parseRunners (text) {
@@ -83,7 +83,7 @@ export function agentTokenExtras (flags) {
83
83
  return out
84
84
  }
85
85
 
86
- const VALUE_FLAGS = new Set(['--repos', '--name', '--org', '--description', '--homepage', '--callback', '--setup', '--webhook', '--events', '--perm', '--authors', '--debounce', '-m', '--installation', '--quilts', '--message', '-n', '--max-count', '--into', '--onto', '--on-conflict', '--quilt', '--square', '--dir', '--line', '--task', '--level', '--title', '--slug', '--format', '--expect-tip', '--side', '--file', '--batch', '--to', '--note', '--after', '--kinds', '--webhook', '--secret', '--branch', '--from', '-S', '--run', '--marks', '--patch', '--on', '--path', '--checks', '--name', '--public-key', '--interval', '--credential', '--as', '--runners', '--run-key', '--state', '--tier', '--tiers', '--tests', '--ci-config', '--commit', '--check', '--log-url', '--at', '--human-only', '--keys', '--reason', '--without', '--allow', '--rules', '--org', '--role', '--agent', '--paths', '--squares', '--hours', '--pre-merge-checks', '--by', '--mode', '--machine', '--add', '--remove', '--exclude', '--max-fetch-bytes', '--expires', '--out', '--auto-combine', '--needs-stack', '--monotonic-stack', '--migrations-in-order', '--rule', '--bisect-width', '--classes', '--min-lines', '-L', '--ignore-rev', '--reverse', '--grep', '-G', '--author', '--on-behalf-of', '--since', '--until', '--group-by', '--cursor', '-U', '--unified', '--renames', '--config', '--only', '--listen', '--idle-exit', '--wait', '--days', '--with', '--stack', '--scope', '--suggest', '--to-line', '--approval-survives', '--stack-defaults', '--stack-keys', '--lfs-url', '--lfs-threshold', '--offset', '--length', '--rate', '--before'])
86
+ const VALUE_FLAGS = new Set(['--repos', '--name', '--org', '--description', '--homepage', '--callback', '--setup', '--webhook', '--events', '--perm', '--authors', '--debounce', '-m', '--installation', '--quilts', '--message', '-n', '--max-count', '--into', '--onto', '--on-conflict', '--quilt', '--square', '--dir', '--line', '--task', '--level', '--title', '--slug', '--format', '--expect-tip', '--side', '--file', '--batch', '--to', '--note', '--after', '--kinds', '--webhook', '--secret', '--branch', '--from', '-S', '--run', '--marks', '--patch', '--on', '--path', '--checks', '--name', '--public-key', '--interval', '--credential', '--as', '--runners', '--run-key', '--state', '--tier', '--tiers', '--tests', '--ci-config', '--commit', '--check', '--log-url', '--at', '--human-only', '--keys', '--reason', '--without', '--allow', '--rules', '--org', '--role', '--agent', '--paths', '--squares', '--hours', '--pre-merge-checks', '--by', '--mode', '--machine', '--add', '--remove', '--exclude', '--max-fetch-bytes', '--expires', '--out', '--auto-combine', '--needs-stack', '--monotonic-stack', '--migrations-in-order', '--rule', '--bisect-width', '--classes', '--min-lines', '-L', '--ignore-rev', '--reverse', '--grep', '-G', '--author', '--on-behalf-of', '--since', '--until', '--group-by', '--cursor', '-U', '--unified', '--renames', '--config', '--only', '--listen', '--idle-exit', '--wait', '--days', '--with', '--stack', '--scope', '--suggest', '--to-line', '--approval-survives', '--stack-defaults', '--stack-keys', '--lfs-url', '--lfs-threshold', '--offset', '--length', '--rate', '--before', '--confirm'])
87
87
 
88
88
  export function parse (argv) {
89
89
  const pos = []; const flags = {}; const paths = []
@@ -147,6 +147,30 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
147
147
  const r = await client.setQuiltOwner(needQuilt(), flags['--org'])
148
148
  return print(r, `${r.quilt} now belongs to ${r.org}.`)
149
149
  }
150
+ if (pos[0] === 'rename') {
151
+ // seam quilts rename <new-slug> --quilt <q> [--title <name>]: its address is <owner>/<new-slug> after.
152
+ if (!pos[1] && !flags['--title']) throw new Error('seam quilts rename <new-slug> --quilt <q> [--title <name>]')
153
+ const r = await client.renameQuilt(needQuilt(), { ...(pos[1] ? { slug: pos[1] } : {}), ...(flags['--title'] ? { name: flags['--title'] } : {}) })
154
+ return print(r, r.slug === r.was ? `${r.slug} is now called ${r.name}.` : `${r.was} is now ${r.path || r.slug}${r.name !== r.slug ? ` (${r.name})` : ''}. Links by its id keep working; its old address doesn't. In a block folder of it, nothing changes: the block names it by id.`)
155
+ }
156
+ if (pos[0] === 'delete') {
157
+ // seam quilts delete <q> --confirm <owner/name> (11.13): hidden at once, restored for 7 days, purged after.
158
+ const q = pos[1] || quilt
159
+ if (!q || !flags['--confirm']) throw new Error('seam quilts delete <q> --confirm <owner/name>: type the repo\'s address to delete it')
160
+ const r = await client.deleteQuilt(q, flags['--confirm'])
161
+ return print(r, describeDeletedQuilt(r))
162
+ }
163
+ if (pos[0] === 'deleted') {
164
+ // seam quilts deleted [--org <org>]: Recently deleted, yours or an org's (its admins and owners).
165
+ const r = await client.deletedQuilts({ org: flags['--org'] })
166
+ return print(r, describeDeletedQuilts(r, { org: flags['--org'] }))
167
+ }
168
+ if (pos[0] === 'restore') {
169
+ // seam quilts restore <slug|id|owner/name> [--org <org>]: back as it was, within 7 days of the delete.
170
+ if (!pos[1]) throw new Error('seam quilts restore <slug, id or owner/name> [--org <org>]')
171
+ const r = await client.undeleteQuilt(pos[1], { org: flags['--org'] })
172
+ return print(r, `Restored ${r.path || r.quilt}: it is back as it was. The squares the delete closed stay closed; seam square reopen <sq> --quilt ${r.quilt} brings one back.`)
173
+ }
150
174
  if (pos[0] === 'sealed') {
151
175
  if (!['on', 'off'].includes(pos[1])) throw new Error('seam quilts sealed on|off --quilt <q>')
152
176
  const r = await client.setSealed(needQuilt(), pos[1] === 'on')
@@ -345,12 +369,9 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
345
369
  }
346
370
  case 'pull': {
347
371
  if (!found) throw new Error('seam pull works in a block folder: run it in a folder made by seam clone or seam checkout.')
372
+ const was = { ...block }
348
373
  const r = await pull(client, found.dir, block)
349
- const kept = describeBoth(r)
350
- if (r.noop) return print(r, block.square ? `Already up to date: ${block.square_short || block.square} sits on ${block.line}'s tip.` : `Already up to date with ${block.line}.`)
351
- if (r.left) return print(r, `${r.left.square} was ${r.left.state}; the folder is now on ${r.line} (${r.at.slice(0, 12)}, ${r.written} file(s) updated).` + kept)
352
- if (r.rebased) return print(r, describeWrite(r.result, 'Rebased') + `\n${r.written} file(s) updated in the folder.` + kept)
353
- return print(r, `Pulled ${r.line} (now ${r.at.slice(0, 12)}): ${r.written} file(s) updated.` + kept)
374
+ return print(r, describePull(r, was))
354
375
  }
355
376
  case 'checkout': {
356
377
  const target = pos[0]
@@ -683,6 +704,12 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
683
704
  const r = await client.editApp(pos[1], b)
684
705
  return print(r, describeApp(r))
685
706
  }
707
+ case 'account': {
708
+ // seam account log [-n <count>] [--before <id>]: what happened to your repos (11.13). seam account delete is the seam package's.
709
+ if (pos[0] !== 'log') throw new Error('seam account log [-n <count>] [--before <id>] | seam account delete --confirm <your email>')
710
+ const r = await client.personLog({ limit: flags['-n'], before: flags['--before'] })
711
+ return print(r, describePersonLog(r))
712
+ }
686
713
  case 'org': {
687
714
  // seam org [list] | new <slug> [--title <name>] | show <org> | add <org> <member> [--role member|admin|owner] | remove <org> <member>
688
715
  const sub = pos[0] || 'list'
@@ -1097,8 +1124,13 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
1097
1124
  case 'export': {
1098
1125
  // To a git remote, on the server, after each merge (14): seam export remote <url> | status.
1099
1126
  if (pos[0] === 'remote' && pos[1]) {
1100
- const r = await client.setExport(needQuilt(), { target: pos[1], lines: flags['--line'] ? String(flags['--line']).split(',') : undefined, releases: !!flags['--releases'], credential: flags['--credential'] || undefined, scratch: !!flags['--scratch'], enabled: !flags['--off'], lfs: flags['--lfs-url'] ? { url: String(flags['--lfs-url']), ...(flags['--lfs-threshold'] != null ? { threshold_bytes: Number(flags['--lfs-threshold']) } : {}) } : undefined })
1101
- return print(r, r.enabled ? `Exporting ${r.lines.join(', ')} to ${r.target} after each merge${r.releases ? ', with release tags' : ''}${r.lfs ? `, files over ${r.lfs.threshold_bytes} bytes to LFS at ${r.lfs.url}` : ''}. seam export status` : `The export to ${r.target} is off.`)
1127
+ const r = await client.setExport(needQuilt(), { target: pos[1], lines: flags['--line'] ? String(flags['--line']).split(',') : undefined, releases: !!flags['--releases'], credential: flags['--credential'] || undefined, scratch: !!flags['--scratch'], enabled: !flags['--off'], lfs: flags['--lfs-url'] ? { url: String(flags['--lfs-url']), ...(flags['--lfs-threshold'] != null ? { threshold_bytes: Number(flags['--lfs-threshold']) } : {}) } : undefined, main_as: flags['--main-as'] || undefined, push: flags['--push'] || undefined, exclude: flags['--exclude'] ? String(flags['--exclude']).split(',').map((x) => x.trim()).filter(Boolean) : undefined })
1128
+ return print(r, r.enabled ? `Exporting ${r.lines.join(', ')} to ${r.target} ${r.push === 'tag' ? 'up to the newest tagged patch' : 'after each merge'}${r.main_as && r.main_as !== 'main' ? `, main as ${r.main_as}` : ''}${r.releases ? ', with release tags' : ''}${r.exclude?.length ? `, without ${r.exclude.join(', ')}` : ''}${r.lfs ? `, files over ${r.lfs.threshold_bytes} bytes to LFS at ${r.lfs.url}` : ''}. seam export status` : `The export to ${r.target} is off.`)
1129
+ }
1130
+ // Runs the export now (admins): after fixing a remote, or without waiting for the next retry.
1131
+ if (pos[0] === 'push') {
1132
+ const r = await client.exportPush(needQuilt())
1133
+ return print(r, `Pushing to ${r.target} now. seam export status`)
1102
1134
  }
1103
1135
  if (pos[0] === 'status') {
1104
1136
  const r = await client.exportStatus(needQuilt())
@@ -237,6 +237,7 @@ export function quiltClient ({ api, token, fetch: fetchImpl = globalThis.fetch,
237
237
  search: (o = {}) => call('GET', `/v1/search${qs(o)}`),
238
238
  searchCode: (q, o = {}) => call('GET', `${Q(q)}/search${qs(o)}`),
239
239
  bisect: (q, b) => call('POST', `${Q(q)}/bisect`, b),
240
+ lineLasts: (q, line = 'main', paths = []) => call('GET', `${Q(q)}/lines/${encodeURIComponent(line)}/lasts${qs({ paths: paths.join(',') })}`),
240
241
  linePatches: (q, line = 'main', o = {}) => call('GET', `${Q(q)}/lines/${encodeURIComponent(line)}/patches${qs(o)}`),
241
242
  diff: (q, o = {}) => call('GET', `${Q(q)}/diff${qs(o)}`),
242
243
  resolutions: async (q, p, { hunks = false } = {}) => (await call('GET', `${Q(q)}/resolutions${qs({ path: p || undefined, hunks: hunks ? 1 : undefined })}`)).resolutions,
@@ -345,6 +346,12 @@ export function quiltClient ({ api, token, fetch: fetchImpl = globalThis.fetch,
345
346
  setOrgTeam: (o, name, members) => call('PUT', `/v1/orgs/${enc(o)}/teams/${enc(name)}`, { members }),
346
347
  setOrgMember: (o, member, role) => String(member).includes('@') ? call('POST', `/v1/orgs/${enc(o)}/members`, { email: member, role }) : call('PUT', `/v1/orgs/${enc(o)}/members/${enc(member)}`, { role }),
347
348
  setQuiltOwner: (q, org) => call('PUT', `${Q(q)}/owner`, { org }),
349
+ renameQuilt: (q, body) => call('PATCH', Q(q), body),
350
+ // Deleting a repo (11.13): confirm is its address, <owner>/<slug>; restored for 7 days by id, slug or address.
351
+ deleteQuilt: (q, confirm) => call('DELETE', `${Q(q)}${qs({ confirm })}`),
352
+ deletedQuilts: ({ org } = {}) => call('GET', org ? `/v1/orgs/${enc(org)}/deleted-quilts` : '/v1/me/deleted-quilts'),
353
+ undeleteQuilt: (q, { org } = {}) => call('POST', `${org ? `/v1/orgs/${enc(org)}` : '/v1/me'}/deleted-quilts/${enc(q)}/restore`, {}),
354
+ personLog: ({ limit, before } = {}) => call('GET', `/v1/me/log${qs({ limit, before })}`),
348
355
  setAgentsMakeLines: (q, allow) => call('PUT', `${Q(q)}/agents-make-lines`, { allow }),
349
356
  setPublic: (q, value) => call('PUT', `${Q(q)}/public`, { public: value }),
350
357
  setSealed: (q, value) => call('PUT', `${Q(q)}/sealed`, { sealed: value }),
@@ -372,6 +379,7 @@ export function quiltClient ({ api, token, fetch: fetchImpl = globalThis.fetch,
372
379
  importKnown: (q, shas) => call('POST', `${Q(q)}/import/known`, { shas }),
373
380
  setExport: (q, b) => call('PUT', `${Q(q)}/export`, b),
374
381
  exportStatus: (q) => call('GET', `${Q(q)}/export`),
382
+ exportPush: (q) => call('POST', `${Q(q)}/export/push`, {}),
375
383
  startImport: (b) => call('POST', '/v1/imports', b),
376
384
  github: () => call('GET', '/v1/github'),
377
385
  importGithub: (b) => call('POST', '/v1/github/import', b),
@@ -19,20 +19,22 @@ export function describeSquare (s) {
19
19
  }
20
20
 
21
21
  export function describeLog (r, { oneline = false, graph = false } = {}) {
22
- const more = r.cursor ? `(more: seam log … --cursor ${r.cursor})` : r.more ? (oneline ? '…' : '(more: seam log -n <count>)') : null
22
+ // A search that ran out of time (stopped: 'time') or of commits to look at ('scan') goes on with its cursor.
23
+ const early = r.partial ? (r.stopped === 'time' ? '(stopped early: the search ran out of time) ' : '(partial) ') : ''
24
+ const more = r.cursor ? (r.partial ? `${early}(search further: seam log … --cursor ${r.cursor})` : `(more: seam log … --cursor ${r.cursor})`) : r.more ? (oneline ? '…' : '(more: seam log -n <count>)') : null
23
25
  if (r.groups) {
24
26
  // group_by: git shortlog -s, with lines added and removed.
25
27
  if (!r.groups.length) return 'No commits.'
26
28
  const w = Math.max(...r.groups.map((g) => String(g.commits).length))
27
- return [...r.groups.map((g) => `${String(g.commits).padStart(w + 2)} ${g.key == null ? '(none)' : g.name ? `${g.name} <${g.key}>` : g.key} +${g.added} -${g.removed}`), ...(more ? [`${r.partial ? '(partial) ' : ''}${more}`] : [])].join('\n')
29
+ return [...r.groups.map((g) => `${String(g.commits).padStart(w + 2)} ${g.key == null ? '(none)' : g.name ? `${g.name} <${g.key}>` : g.key} +${g.added} -${g.removed}`), ...(more ? [more] : [])].join('\n')
28
30
  }
29
- if (!r.commits.length) return more ? `No commits on this page.\n${more}` : r.existed === false ? 'No commits: this path was never here.' : 'No commits.'
31
+ if (!r.commits.length) return more ? `${r.partial ? 'None found yet.' : 'No commits on this page.'}\n${more}` : r.existed === false ? 'No commits: this path was never here.' : 'No commits.'
30
32
  const mark = (c) => graph ? (c.parents.length > 1 ? '* ' : c.hidden ? 'x ' : '* ') : ''
31
33
  const merged = (c) => (c.parent_squares || []).map((s) => `${graph ? '|\\ ' : ''}merged ${s.square_id ? `square ${s.square_id.slice(0, 8)}${s.title ? ` "${s.title}"` : ''}` : s.commit.slice(0, 12)}`)
32
34
  const lines = r.commits.map((c) => oneline
33
35
  ? [`${mark(c)}${c.short} ${c.patch ? `[${c.patch.line}:${c.patch.patch_no}] ` : ''}${first(c.message)}${r.follow && c.path && c.path !== r.follow ? ` (${c.path})` : ''}${c.hidden ? ` (hidden: replaced by ${c.predecessor_of.slice(0, 12)})` : ''}${c.signature ? ` (signature: ${c.signature.status})` : ''}`, ...merged(c)].join('\n')
34
36
  : [`${mark(c)}commit ${c.id}${c.patch ? ` (${patchName(c.patch)})` : ''}${c.hidden ? ` (hidden: replaced by ${c.predecessor_of.slice(0, 12)})` : ''}`, ...(c.parents.length > 1 ? [`Merge: ${c.parents.map((p) => p.slice(0, 12)).join(' ')}`, ...merged(c)] : []), `Square: ${c.square}`, ...(r.follow && c.path && c.path !== r.follow ? [`Path: ${c.path}`] : []), `Author: ${c.author.name} <${c.author.member_id}>${c.author.tool ? ` via ${c.author.tool}` : ''}${c.on_behalf_of ? ` for ${c.on_behalf_of}` : ''}`, ...(c.task_id ? [`Task: ${c.task_id}`] : []), ...(c.signature ? [describeSignature(c.signature)] : []), `Date: ${when(c.authored_at)}`, '', ...String(c.message || '').split('\n').map((l) => ` ${l}`), ''].join('\n'))
35
- if (more) lines.push(`${r.partial ? '(partial) ' : ''}${more}`)
37
+ if (more) lines.push(more)
36
38
  if (r.last_present !== undefined) lines.push(`This path is gone: it was last here in patch ${r.last_present}.`)
37
39
  return lines.join('\n')
38
40
  }
@@ -111,9 +113,24 @@ export function describeBoth (r, { diff = 'seam diff', resolve = 'seam resolve <
111
113
  const out = []
112
114
  if (r.merged?.length) out.push(`Merged upstream's changes into your edits to ${r.merged.join(', ')}: compare with ${diff}.`)
113
115
  if (r.conflicted?.length) out.push(`Conflict in ${r.conflicted.join(', ')} (changed on both sides): settle each by editing out the conflict markers, or with ${resolve}, then commit.`)
116
+ if (r.held?.length) out.push(`The square holds a conflict in ${r.held.join(', ')}: settle each by editing out the conflict markers, or with ${resolve}, then commit.`)
114
117
  return out.map((l) => '\n' + l).join('')
115
118
  }
116
119
 
120
+ /**
121
+ * seam pull's answer (block.js pull), the same for the CLI and seam mcp. `block` is the folder's
122
+ * record before the pull; `diff` and `resolve` as describeBoth.
123
+ */
124
+ export function describePull (r, block, names) {
125
+ const kept = describeBoth(r, names)
126
+ if (r.noop) return block.square ? `Already up to date: ${block.square_short || block.square} sits on ${block.line}'s tip.` : `Already up to date with ${block.line}.`
127
+ if (r.left) return `${r.left.square} was ${r.left.state}; the folder is now on ${r.line} (${r.at.slice(0, 12)}, ${r.written} file(s) updated).` + kept
128
+ const caught = r.caught_up ? `Brought ${r.square.short}'s new head in (${String(r.caught_up.to).slice(0, 12)}): the square moved on the server since this folder last looked.` : ''
129
+ if (r.rebased) return (caught ? caught + '\n' : '') + describeWrite(r.result, 'Rebased') + `\n${r.written} file(s) updated in the folder.` + kept
130
+ if (caught) return `${caught} ${r.written} file(s) updated; it sits on ${r.square.line}'s tip.` + kept
131
+ return `Pulled ${r.line} (now ${r.at.slice(0, 12)}): ${r.written} file(s) updated.` + kept
132
+ }
133
+
117
134
  export function describeStatus (st) {
118
135
  const lines = []
119
136
  if (st.square) lines.push(describeSquareStatus(st.square, { prefix: 'On square ' }))
@@ -121,7 +138,7 @@ export function describeStatus (st) {
121
138
  else lines.push(`A read-only checkout of ${st.at ? st.at.slice(0, 12) : 'an empty line'}`)
122
139
  lines.push(`Block: ${st.paths.map((p) => p || '/').join(', ')} at ${st.at ? st.at.slice(0, 12) : '(empty)'}`)
123
140
  if (st.square_closed) lines.push(`This square is ${st.square_closed}: seam pull moves the folder to ${st.square.line}'s tip.`)
124
- if (st.stale) lines.push('The square moved on since this folder last looked: run seam checkout ' + st.square.short + ' after committing.')
141
+ if (st.stale) lines.push('The square moved on since this folder last looked: seam pull brings its new head in (your uncommitted edits stay).')
125
142
  if (st.conflicted.length) lines.push(`Conflicted (edit and commit without the markers, or seam resolve --ours|--theirs): ${st.conflicted.join(', ')}`)
126
143
  if (!st.changes.length) lines.push('Nothing changed since the last commit.')
127
144
  else lines.push('Changes not yet committed:', ...st.changes.map((c) => ` ${c.status.padEnd(9)} ${c.path}`))
@@ -572,6 +589,31 @@ export const describeInstalls = (r) => r.installations.length ? r.installations.
572
589
 
573
590
  export const describeApps = (r) => r.apps.length ? r.apps.map((a) => describeApp(a)).join('\n\n') : 'No apps.'
574
591
 
592
+ // The rows deleting a repo writes in its owner's log (11.13), the org's and a person's alike.
593
+ const QUILT_LOG = {
594
+ quilt_deleted: (d) => `deleted the repo ${d.slug}${d.closed ? ` (closed ${d.closed} agent square${d.closed === 1 ? '' : 's'})` : ''}`,
595
+ quilt_restored: (d) => `restored the repo ${d.slug}`,
596
+ quilt_purged: (d) => `purged the repo ${d.slug}${d.reason === 'account_deleted' ? ' (the account was deleted)' : ', 7 days after it was deleted'}`
597
+ }
598
+
599
+ /** A deleted repo (11.13): when it is purged, and how to bring it back. */
600
+ export function describeDeletedQuilt (r) {
601
+ const org = String(r.owner || '').startsWith('org:') && r.path ? r.path.split('/')[0] : null
602
+ return `Deleted ${r.path || r.quilt}: nobody reaches it now, and its address stays ${org ? `${org}'s` : 'yours'}. Restore it until ${when(r.purge_at)} with seam quilts restore ${r.quilt}${org ? ` --org ${org}` : ''}; after that it is purged.${r.closed_squares ? ` ${r.closed_squares} square${r.closed_squares === 1 ? '' : 's'} agents held closed.` : ''}`
603
+ }
604
+
605
+ /** Recently deleted: yours or an org's, each with when it is purged. */
606
+ export function describeDeletedQuilts (r, { org } = {}) {
607
+ if (!r.quilts.length) return `No deleted repos${org ? ` in ${org}` : ''}: a deleted repo stays here 7 days.`
608
+ return r.quilts.map((q) => `${(q.path || q.slug).padEnd(24)} deleted ${when(q.deleted_at)} by ${q.deleted_by}, purged ${when(q.purge_at)} (seam quilts restore ${q.slug}${org ? ` --org ${org}` : ''})`).join('\n')
609
+ }
610
+
611
+ /** A person's log (11.13): what happened to their repos, newest first. */
612
+ export function describePersonLog (r) {
613
+ if (!r.log.length) return 'Your log is empty.'
614
+ return [...r.log.map((x) => `${x.id.padStart(6)} ${new Date(x.at).toISOString().slice(0, 16).replace('T', ' ')} ${x.actor} ${(QUILT_LOG[x.kind] || ((d) => `${x.kind} ${JSON.stringify(d)}`))(x.data || {})}`), ...(r.next ? [`More: seam account log --before ${r.next}`] : [])].join('\n')
615
+ }
616
+
575
617
  export function describeOrgLog (r) {
576
618
  const what = (x) => {
577
619
  const d = x.data || {}
@@ -581,6 +623,7 @@ export function describeOrgLog (r) {
581
623
  if (x.kind.startsWith('app_')) return `${{ app_created: 'made', app_changed: 'changed', app_removed: 'removed', app_manager_added: `made ${d.member} a manager of`, app_manager_removed: `took ${d.member} off the managers of`, app_key_added: `made a key ${d.key_id} for`, app_key_removed: `removed the key ${d.key_id} of`, app_moved_in: `moved in from ${d.from}` }[x.kind] || x.kind}${x.kind.startsWith('app_key') || x.kind.startsWith('app_manager') || x.kind === 'app_moved_in' ? '' : ' the app'} ${d.handle} ${d.handle}${d.fields?.length ? ` (${d.fields.join(', ')})` : ''}`
582
624
  if (x.kind === 'app_installed' || x.kind === 'app_uninstalled' || x.kind.startsWith('install_')) return `${{ app_installed: 'installed', app_uninstalled: 'removed', install_requested: 'asked for', install_request_approved: 'approved the request for', install_request_declined: 'declined the request for', install_repos_changed: 'changed the repos of', install_paused: 'paused', install_permissions_accepted: 'accepted new permissions for', install_resumed: 'resumed' }[x.kind] || x.kind} the app ${d.handle}${['app_installed', 'install_repos_changed', 'install_requested'].includes(x.kind) ? ` (${d.repos ? d.repos.join(', ') : 'all repos'})` : ''}`
583
625
  if (x.kind.startsWith('domain_')) return `${{ domain_added: 'added', domain_verified: 'verified', domain_lost: 'lost (its record went away)', domain_removed: 'removed' }[x.kind] || x.kind} the domain ${d.domain}`
626
+ if (x.kind.startsWith('quilt_') && QUILT_LOG[x.kind]) return QUILT_LOG[x.kind](d)
584
627
  if (x.kind === 'quilt_in') return `moved the quilt ${d.slug} in (from ${d.from})`
585
628
  if (x.kind === 'quilt_out') return `moved the quilt ${d.slug} out (to ${d.to})`
586
629
  return `${x.kind} ${JSON.stringify(d)}`
@@ -61,9 +61,10 @@ async function fetchBlobs (client, q, ids) {
61
61
  * missing). Returns { exported, patch_no, sha, branch }. With `sign` (export-sign.js exportKey), every
62
62
  * commit carries an SSH signature with that key. With `lfs` ({ threshold, upload(bytes) → pointer
63
63
  * text }, export-lfs.js), a file over the threshold or named by a `filter=lfs` line of the root
64
- * `.gitattributes` is written as an LFS pointer and its bytes uploaded.
64
+ * `.gitattributes` is written as an LFS pointer and its bytes uploaded. With `exclude` (a function
65
+ * of a path), the paths it names are left out of every exported tree (14, `exclude`).
65
66
  */
66
- export async function exportGit (client, { q, dir, line = 'main', branch, page = 200, sign = null, lfs = null, log = () => {} }) {
67
+ export async function exportGit (client, { q, dir, line = 'main', branch, page = 200, sign = null, lfs = null, exclude = null, log = () => {} }) {
67
68
  branch = branch || line
68
69
  const gitDir = openRepo(dir)
69
70
  const stateFile = path.join(gitDir, 'quilt-export.json')
@@ -89,7 +90,7 @@ export async function exportGit (client, { q, dir, line = 'main', branch, page =
89
90
  if (version !== null && version !== r.version) throw new Error(`export format ${r.version} differs from the ${version} this branch was exported with: export to a new branch`)
90
91
  version = r.version; quiltId = r.quilt
91
92
  if (!r.patches.length) break
92
- const blobs = await fetchBlobs(client, q, r.patches.flatMap((p) => p.changes.filter((c) => c.op === 'put').map((c) => c.blob)))
93
+ const blobs = await fetchBlobs(client, q, r.patches.flatMap((p) => p.changes.filter((c) => c.op === 'put' && !(exclude && exclude(c.path))).map((c) => c.blob)))
93
94
  const marksFile = path.join(gitDir, 'quilt-export.marks')
94
95
  const fi = fastImport(dir, marksFile)
95
96
  const send = async (s) => { const w = fi.write(s); if (w) await w }
@@ -116,6 +117,7 @@ export async function exportGit (client, { q, dir, line = 'main', branch, page =
116
117
  if (attrs) lfsMatch = attrs.op === 'delete' ? null : lfsPatterns(blobs.get(attrs.blob)?.toString('utf8'))
117
118
  }
118
119
  for (const c of p.changes) {
120
+ if (exclude && exclude(c.path)) continue
119
121
  const bytes = lfs && c.op === 'put' && (!c.mode || c.mode === 'file' || c.mode === 'exec') && c.path !== '.gitattributes' ? blobs.get(c.blob) : null
120
122
  if (bytes && (bytes.length > lfs.threshold || lfsMatch?.ignores(c.path))) {
121
123
  const pointer = await lfs.upload(bytes)
@@ -16,10 +16,10 @@ import { eventLine, describeLog, describeShow, describeDiff, describeBlame, desc
16
16
  import { signWithLocalKey, localKey, makeLocalKey, signingKeyFile } from './signing.js'
17
17
  import { startDaemon, askDaemon } from './daemon.js'
18
18
  import { rebuild as rebuildBlock, conflictStyle } from './block.js'
19
- import { describeRebuild, describeOpened, describeBoth } from './describe.js'
19
+ import { describeRebuild, describeOpened, describeBoth, describePull, describeDeletedQuilt, describeDeletedQuilts, describePersonLog } from './describe.js'
20
20
  import { status as blockStatus, commitFolder, amendFolder, syncTo, pull, toLineTip, checkout, restore, reset, clean, localChanges, bytesOf, livePatch, resolvePulled } from './block.js'
21
21
 
22
- export const SEAM_TOOLS = ['seam_quilts', 'seam_open_square', 'seam_squares', 'seam_log', 'seam_show', 'seam_diff', 'seam_blame', 'seam_cat', 'seam_ls', 'seam_commit', 'seam_merge', 'seam_rebase', 'seam_revert', 'seam_cherry_pick', 'seam_resolve', 'seam_square_status', 'seam_square_handoff', 'seam_abandon', 'seam_square_merged', 'seam_reopen', 'seam_fold', 'seam_split', 'seam_events', 'seam_events_webhook', 'seam_line_list', 'seam_line_new', 'seam_line_freeze', 'seam_line_unfreeze', 'seam_line_require', 'seam_reviews', 'seam_review', 'seam_tag_list', 'seam_tag', 'seam_release_list', 'seam_release', 'seam_import_git', 'seam_import_remote', 'seam_github', 'seam_github_import', 'seam_import_status', 'seam_import_authors', 'seam_import_poke', 'seam_import_cutover', 'seam_export_remote', 'seam_export_status', 'seam_restore', 'seam_reset', 'seam_clean', 'seam_live', 'seam_live_join', 'seam_live_set', 'seam_bisect', 'seam_grep', 'seam_search', 'seam_describe', 'seam_merge_base', 'seam_stack', 'seam_migrations_dir', 'seam_claims', 'seam_claim', 'seam_unclaim', 'seam_check', 'seam_resolutions', 'seam_forget_resolution', 'seam_sign', 'seam_keys', 'seam_key_add', 'seam_key_remove', 'seam_token_list', 'seam_token_new', 'seam_token_revoke', 'seam_protections', 'seam_protections_set', 'seam_team_set', 'seam_verify', 'seam_redact', 'seam_redactions', 'seam_seal', 'seam_secret_scanning', 'seam_public', 'seam_sealed', 'seam_policy', 'seam_line_policy_versions', 'seam_orgs', 'seam_org_log', 'seam_org_domain', 'seam_apps', 'seam_app', 'seam_app_key', 'seam_app_manager', 'seam_app_install', 'seam_app_request', 'seam_org_member', 'seam_org_team', 'seam_agent_token_new', 'seam_agent_token_list', 'seam_agent_token_revoke', 'seam_agents_make_lines', 'seam_checks', 'seam_check_rerun', 'seam_reparent', 'seam_archive', 'seam_unarchive', 'seam_hide', 'seam_unhide', 'seam_square_edit', 'seam_share', 'seam_unshare', 'seam_export_tarball', 'seam_claim_request', 'seam_claim_withdraw', 'seam_claim_handoff', 'seam_claim_finish', 'seam_blocks', 'seam_block_open', 'seam_block_touch', 'seam_block_close', 'seam_ancestor', 'seam_contains', 'seam_pipeline', 'seam_watch', 'seam_unwatch', 'seam_absorb', 'seam_interdiff', 'seam_apply_suggestion', 'seam_infra_paths', 'seam_owner_idle_hours', 'seam_public_reads', 'seam_export_archive', 'seam_metrics', 'seam_daemon', 'seam_block_rebuild', 'seam_clone', 'seam_pull', 'seam_member_add']
22
+ export const SEAM_TOOLS = ['seam_quilts', 'seam_open_square', 'seam_squares', 'seam_log', 'seam_show', 'seam_diff', 'seam_blame', 'seam_cat', 'seam_ls', 'seam_commit', 'seam_merge', 'seam_rebase', 'seam_revert', 'seam_cherry_pick', 'seam_resolve', 'seam_square_status', 'seam_square_handoff', 'seam_abandon', 'seam_square_merged', 'seam_reopen', 'seam_fold', 'seam_split', 'seam_events', 'seam_events_webhook', 'seam_line_list', 'seam_line_new', 'seam_line_freeze', 'seam_line_unfreeze', 'seam_line_require', 'seam_reviews', 'seam_review', 'seam_tag_list', 'seam_tag', 'seam_release_list', 'seam_release', 'seam_import_git', 'seam_import_remote', 'seam_github', 'seam_github_import', 'seam_import_status', 'seam_import_authors', 'seam_import_poke', 'seam_import_cutover', 'seam_export_remote', 'seam_export_status', 'seam_export_push', 'seam_restore', 'seam_reset', 'seam_clean', 'seam_live', 'seam_live_join', 'seam_live_set', 'seam_bisect', 'seam_grep', 'seam_search', 'seam_describe', 'seam_merge_base', 'seam_stack', 'seam_migrations_dir', 'seam_claims', 'seam_claim', 'seam_unclaim', 'seam_check', 'seam_resolutions', 'seam_forget_resolution', 'seam_sign', 'seam_keys', 'seam_key_add', 'seam_key_remove', 'seam_token_list', 'seam_token_new', 'seam_token_revoke', 'seam_protections', 'seam_protections_set', 'seam_team_set', 'seam_verify', 'seam_redact', 'seam_redactions', 'seam_seal', 'seam_secret_scanning', 'seam_public', 'seam_sealed', 'seam_policy', 'seam_line_policy_versions', 'seam_orgs', 'seam_org_log', 'seam_org_domain', 'seam_apps', 'seam_app', 'seam_app_key', 'seam_app_manager', 'seam_app_install', 'seam_app_request', 'seam_org_member', 'seam_org_team', 'seam_agent_token_new', 'seam_agent_token_list', 'seam_agent_token_revoke', 'seam_agents_make_lines', 'seam_checks', 'seam_check_rerun', 'seam_reparent', 'seam_archive', 'seam_unarchive', 'seam_hide', 'seam_unhide', 'seam_square_edit', 'seam_share', 'seam_unshare', 'seam_export_tarball', 'seam_claim_request', 'seam_claim_withdraw', 'seam_claim_handoff', 'seam_claim_finish', 'seam_blocks', 'seam_block_open', 'seam_block_touch', 'seam_block_close', 'seam_ancestor', 'seam_contains', 'seam_pipeline', 'seam_watch', 'seam_unwatch', 'seam_absorb', 'seam_interdiff', 'seam_apply_suggestion', 'seam_infra_paths', 'seam_owner_idle_hours', 'seam_public_reads', 'seam_export_archive', 'seam_metrics', 'seam_daemon', 'seam_block_rebuild', 'seam_clone', 'seam_pull', 'seam_member_add', 'seam_quilt_delete', 'seam_quilts_deleted', 'seam_quilt_restore', 'seam_account_log']
23
23
 
24
24
  /** What every agent is told about source control, in each MCP's instructions. */
25
25
  export const SEAM_GUIDE = 'Quilt source control (quilts): a quilt is a repository on the Quilt server; you work in a square (your own branch and working copy), commit save points in it, and merge it into main, where it becomes a patch. ' +
@@ -71,6 +71,7 @@ export const SEAM_DESCRIPTIONS = {
71
71
  seam_export_archive: 'Writes the whole quilt as one .tar (admin) to a file on this computer: every log segment, every pack and the members, with a manifest of sizes and BLAKE3s; out (default <quilt>-<row>.seam.tar). seam export check <file> checks it offline. Over HTTP: POST /v1/quilts/:q/exit/archive.',
72
72
  seam_export_tarball: 'Writes one commit as a .tar.gz (what git archive makes) to a file on this computer: rev (default main), paths to take only part, out (default <quilt>-<commit>.tar.gz in this folder). The same commit always gives the same bytes. Over HTTP: GET /v1/quilts/:q/tarball/:rev?paths=.',
73
73
  seam_export_status: 'The export to a git remote: its target, lines, state (idle, ok, retrying, refused), each line\'s last exported patch and sha, and the last error.',
74
+ seam_export_push: 'Runs the export to a git remote now (admins), without waiting for the next merge or retry.',
74
75
  seam_import_cutover: 'Ends a following import for good (admins): a last pass, a check that the remote\'s tips equal the imported ones, then main and release/* take merges in Seam and main exports to the source after each merge. Refuses with remote_moved if git moved.',
75
76
  seam_import_status: 'An import from a git remote: its state (reading, awaiting_authors, following, done, failed), its lines with their git tips, its last error, and while it waits the authors with the proposed member of each.',
76
77
  seam_import_authors: 'Answers an import waiting in awaiting_authors (admins): a map of git author email → member id of the quilt; the import then runs. An email left out becomes a stub member. Refuses an id that is not a member (unknown_author).',
@@ -84,7 +85,7 @@ export const SEAM_DESCRIPTIONS = {
84
85
  seam_block_touch: 'The heartbeat of an open block: keeps it open (blocks untouched for seven days are closed) and moves a live block to the square\'s current head; add and remove change its folders.',
85
86
  seam_clone: 'Checks out a line of a quilt (main by default) into a new folder on this computer, ready to work: the first commit there opens a square by itself. The folder is named after the quilt unless dir is given.',
86
87
  seam_member_add: 'Adds a member to the quilt or changes their level (an admin): email (a person; one with no Seam account yet is invited, and is in the quilt from their first sign-in) or member_id (person:… or agent:…), level read, write or admin (default write; remove takes them out).',
87
- seam_pull: 'In a block folder: brings the line\'s new patches in (uncommitted edits stay; a file changed on both sides is merged, or given conflict markers to settle before it is committed); on a square, rebases it onto its line\'s tip; on a square merged from elsewhere, moves the folder to the line\'s tip.',
88
+ seam_pull: 'In a block folder: brings the line\'s new patches in (uncommitted edits stay; a file changed on both sides is merged, or given conflict markers to settle before it is committed); on a square, brings in its new head if it moved on the server (a merge on the site, conflicts included), then rebases it onto its line\'s tip; on a square merged from elsewhere, moves the folder to the line\'s tip.',
88
89
  seam_block_rebuild: 'In a block folder: finishes a move of the folder (rebase, checkout of its square) that stopped part way, from the journal in .seam; edits made meanwhile are kept as yours. On a whole folder it writes nothing.',
89
90
  seam_block_close: 'Closes a block (its row on the server). The square, its work and any files on disk are untouched.',
90
91
  seam_token_list: 'The quilt\'s tokens (keys for one quilt, for CI and scripts): id, name, read or write, and whether revoked. Admins only.',
@@ -96,6 +97,10 @@ export const SEAM_DESCRIPTIONS = {
96
97
  seam_redact: 'Redacts content from the quilt for good (a person who is an admin): the blob\'s bytes leave every pack, every read gets a tombstone, and the same bytes are refused if added again. History and ids stay. Name blob, or path (and rev). Cannot be undone: confirm must be true.',
97
98
  seam_seal: 'A sealed quilt\'s key (the server never sees its contents): status (key version, who holds it), or grant member: wrap the key this computer holds for that member\'s computers.',
98
99
  seam_secret_scanning: 'Reads, or (a person who is an admin) sets, secret scanning on the quilt: refuse (a commit that looks like it holds a secret is refused with path, line and finding id), warn, or off.',
100
+ seam_quilt_delete: 'Deletes a repo (a person: its owner, or an owner or admin of the org that owns it; never an agent on its own). confirm is the repo\'s address as <owner>/<name>, typed by the person who asked. It is hidden at once (it answers as a missing repo; its tokens, webhooks and app installs stop; agents\' open squares in it close) and can be restored for 7 days with seam_quilt_restore; then it is purged. Export it first if anything in it is wanted (seam export archive, or the export to git).',
101
+ seam_quilts_deleted: 'Lists Recently deleted repos: yours, or an org\'s (org: its slug; its admins and owners), each with when it was deleted, by whom, and when it is purged.',
102
+ seam_quilt_restore: 'Restores a repo deleted in the last 7 days, as it was (its address, members, settings, tokens and people\'s squares; the squares the delete closed stay closed, seam_reopen brings one back). quilt: its slug, id or address; org: when an org owned it.',
103
+ seam_account_log: 'What happened to your repos, newest first: deleted, restored, purged; who did it and when (an org\'s are in seam_org_log). before: a row id, for the page after.',
99
104
  seam_public: 'Makes the quilt readable by anyone, signed in or not (public: true), or private again (a person who is an admin). The quilt\'s protections can still keep paths out of public view.',
100
105
  seam_policy: 'A square\'s change classes (whitespace, comment-only, docs, test-only, dependency, ci-config, schema, logic, per file) and what its line\'s rules by path and class need before it merges: approvals from whom, checks, and which are met.',
101
106
  seam_line_policy_versions: 'A line\'s policy versions (every change to what it needs is one), or, for a person with admin: approve or reject a pending version someone else proposed, or confirm your own with this computer\'s signing key when you are the only admin. Agents read them only.',
@@ -140,7 +145,7 @@ export const SEAM_DESCRIPTIONS = {
140
145
  seam_absorb: 'Puts your square\'s uncommitted live work into the commit of the square that last changed each file (amending it and rewriting the commits above it), so a fix to commit 2 of 5 needs no manual rebase. Files no commit of the square changed stay live.',
141
146
  seam_fold: 'Folds a square (square) into another square you hold (into): its work becomes one commit there and the square closes. Live work it shared (seam_live_set) is committed on it first and goes along (commit_live: false refuses with live_work instead). Overlapping changes become conflict markers in the target.',
142
147
  seam_split: 'Splits your square: by paths (groups: [[paths], ...]) or by claim (one group per claim on the square that covers a changed path; the claim and its holder go with it). Each group moves to a new square (same base) as one commit; the largest group, and paths no group names, stay in this square. stacked: true chains the new squares on this one in order instead of opening them side by side.',
143
- seam_log: 'History, newest first: rev (default main, or your square, or a..b), paths to limit it, limit (default 20). follow: true with one path follows the file through its renames; search: "<text>" keeps only commits that changed how many times the text appears (git log -S); match: "<regex>" keeps commits whose added or removed lines match (git log -G). message (with message_regex, ignore_case) filters on the commit message (git log --grep); author (member id or part of a name), on_behalf_of, task, since and until (an ISO date, or 2h, 3d, 1w) filter on who, what for and when. group_by (author, on_behalf_of, task, tool) gives counts with lines added and removed instead of commits; graph: true names the square each merge brought in; include_hidden: true shows a square\'s commits replaced by an amend. When there is more, pass the answer\'s cursor for the next page. Each patch on main is one line\'s merge. With one path on a line, existed says whether it was ever there and, if it is gone now, last_present is the last patch it was there in.',
148
+ seam_log: 'History, newest first: rev (default main, or your square, or a..b), paths to limit it, limit (default 20). follow: true with one path follows the file through its renames; search: "<text>" keeps only commits that changed how many times the text appears (git log -S); match: "<regex>" keeps commits whose added or removed lines match (git log -G). message (with message_regex, ignore_case) filters on the commit message (git log --grep); author (member id or part of a name), on_behalf_of, task, since and until (an ISO date, or 2h, 3d, 1w) filter on who, what for and when. group_by (author, on_behalf_of, task, tool) gives counts with lines added and removed instead of commits; graph: true names the square each merge brought in; include_hidden: true shows a square\'s commits replaced by an amend. When there is more, pass the answer\'s cursor for the next page. A search (search, match, a regex message) looks for about a second: past that it answers what it found so far, says it stopped early, and gives a cursor to search further. Each patch on main is one line\'s merge. With one path on a line, existed says whether it was ever there and, if it is gone now, last_present is the last patch it was there in.',
144
149
  seam_describe: 'Names a revision (rev, default main) by the nearest tag behind it: v1.0 on the tag itself, v1.0-3-<commit> three commits past it, the commit alone when no tag is behind it.',
145
150
  seam_merge_base: 'Where two revisions (a, b: lines, squares, patches, commits) last shared history: the commit a merge of one into the other starts from.',
146
151
  seam_stack: 'The full-stack pointers at a revision (a line, patch:n, sq:…, a commit): each key with its value, where it was set (a patch number or c:<commit>) and how (set by a commit, or derived: db_migration is the newest .sql file under supabase/migrations). Read it to know which schema and database the code at a patch runs against.',
@@ -200,10 +205,11 @@ export const SEAM_INPUTS = {
200
205
  seam_github: {},
201
206
  seam_github_import: { repo: z.string().max(201).describe('owner/name, as seam_github lists it'), installation: z.string().max(20).describe('The installation id seam_github shows for it'), into: z.string().max(63).optional().describe('The new quilt\'s slug (default the repo\'s name)'), follow: z.boolean().optional().describe('Keep following it (default true)') },
202
207
  seam_import_remote: { url: z.string().max(2000).describe('The git remote: https://, ssh:// or user@host:path'), into: z.string().max(63).describe('The new quilt\'s slug'), follow: z.boolean().optional().describe('Keep following main and release/* until cut-over'), interval: z.number().int().min(60).optional().describe('Seconds between follow passes (default 600)'), credential: z.string().max(100).optional().describe('The name of the server secret that reads the remote (user:token or a token)'), authors: z.record(z.string(), z.string()).optional().describe('Git author email → member id of the quilt'), ask_authors: z.boolean().optional().describe('Wait in awaiting_authors with the authors and a proposed map until seam_import_authors answers') },
203
- seam_export_remote: { quilt: z.string().max(100).optional().describe('The quilt'), target: z.string().max(2000).describe('The git remote'), lines: z.array(z.string().max(100)).max(20).optional().describe('Lines to export (default ["main"]); release/* for every release line'), releases: z.boolean().optional().describe('Push release tags too'), credential: z.string().max(100).optional().describe('The name of the server secret that may push'), scratch: z.boolean().optional().describe('A scratch remote nothing pins a sha from'), enabled: z.boolean().optional().describe('false turns the export off'), lfs: z.object({ url: z.string().max(2000).describe('The LFS endpoint (…/info/lfs)'), threshold_bytes: z.number().int().min(0).optional().describe('Files over this many bytes go to LFS (default 50 MiB)') }).optional().describe('Git LFS on the remote: big files and filter=lfs paths go as LFS pointers. Set before the first export') },
208
+ seam_export_remote: { quilt: z.string().max(100).optional().describe('The quilt'), target: z.string().max(2000).describe('The git remote'), lines: z.array(z.string().max(100)).max(20).optional().describe('Lines to export (default ["main"]); release/* for every release line'), releases: z.boolean().optional().describe('Push release tags too'), credential: z.string().max(100).optional().describe('The name of the server secret that may push'), scratch: z.boolean().optional().describe('A scratch remote nothing pins a sha from'), enabled: z.boolean().optional().describe('false turns the export off'), lfs: z.object({ url: z.string().max(2000).describe('The LFS endpoint (…/info/lfs)'), threshold_bytes: z.number().int().min(0).optional().describe('Files over this many bytes go to LFS (default 50 MiB)') }).optional().describe('Git LFS on the remote: big files and filter=lfs paths go as LFS pointers. Set before the first export'), main_as: z.string().max(100).optional().describe('The branch name main has on the remote (default main), such as master or trunk'), push: z.enum(['patch', 'tag']).optional().describe('patch (default): after every merged patch; tag: only up to the newest patch a tag or release points at'), exclude: z.array(z.string().max(500)).max(100).optional().describe('Paths left out of the exported tree: a folder ending in /, a file or a glob. Set before the first export') },
204
209
  seam_export_archive: { quilt: z.string().max(100).optional().describe('The quilt'), out: z.string().max(1000).optional() },
205
210
  seam_export_tarball: { quilt: z.string().max(100).optional().describe('The quilt'), rev: z.string().max(200).optional(), paths: z.array(z.string().max(1000)).max(100).optional(), out: z.string().max(1000).optional() },
206
211
  seam_export_status: { quilt: z.string().max(100).optional().describe('The quilt') },
212
+ seam_export_push: { quilt: z.string().max(100).optional().describe('The quilt') },
207
213
  seam_import_cutover: { id: z.string().max(100).describe('The import id'), credential: z.string().max(100).optional().describe('The server secret that may push to the source (default the import\'s)') },
208
214
  seam_import_status: { id: z.string().max(100).describe('The import id') },
209
215
  seam_import_authors: { id: z.string().max(100).describe('The import id'), authors: z.record(z.string(), z.string()).describe('Git author email → member id of the quilt') },
@@ -247,6 +253,10 @@ export const SEAM_INPUTS = {
247
253
  seam_line_policy_versions: { quilt: QUILT, line: z.string().max(120), action: z.enum(['list', 'approve', 'reject', 'confirm']).optional(), version: z.number().int().min(1).optional() },
248
254
  seam_policy: { quilt: QUILT, square: z.string().max(100).optional(), line: z.string().max(120).optional() },
249
255
  seam_public: { quilt: QUILT, public: z.boolean() },
256
+ seam_quilt_delete: { quilt: QUILT, confirm: z.string().max(200).describe('The repo\'s address, <owner>/<name>, exactly as the person typed it') },
257
+ seam_quilts_deleted: { org: z.string().max(100).optional().describe('An org you run: its deleted repos') },
258
+ seam_quilt_restore: { quilt: z.string().max(200).describe('The deleted repo: its slug, id or <owner>/<name>'), org: z.string().max(100).optional().describe('The org that owned it') },
259
+ seam_account_log: { limit: z.number().int().min(1).max(500).optional(), before: z.string().regex(/^\d{1,19}$/).optional() },
250
260
  seam_sealed: { quilt: QUILT, sealed: z.boolean() },
251
261
  seam_secret_scanning: { quilt: QUILT, mode: z.enum(['refuse', 'warn', 'off']).optional() },
252
262
  seam_seal: { quilt: QUILT, action: z.enum(['status', 'grant']).optional(), member: z.string().max(200).optional().describe('For grant: the member id to give the key to') },
@@ -530,7 +540,8 @@ export async function runSeamTool (name, args, { client, found = null, hosted =
530
540
  }
531
541
  case 'seam_github_import': { const r = await client.importGithub({ installation_id: args.installation, repo: args.repo, slug: args.into, follow: args.follow !== false }); return `Importing ${args.repo} into ${r.id ? (args.into || args.repo.split('/')[1]) : '?'} (import ${r.id})${r.follow ? ', following it' : ''}.` }
532
542
  case 'seam_import_remote': { const r = await client.startImport({ url: args.url, slug: args.into, follow: !!args.follow, interval: args.interval, credential: args.credential, authors: args.authors, ask_authors: !!args.ask_authors }); return `Importing ${args.url} into ${args.into} (import ${r.id})${r.ask_authors ? ', waiting for its authors first (seam_import_status, then seam_import_authors)' : ''}${r.follow ? `, following every ${r.interval_s} s; poke secret ${r.poke_secret} (shown once)` : ''}.` }
533
- case 'seam_export_remote': { const r = await client.setExport(needQuilt(), { target: args.target, lines: args.lines, releases: args.releases, credential: args.credential, scratch: args.scratch, enabled: args.enabled !== false, lfs: args.lfs }); return r.enabled ? `Exporting ${r.lines.join(', ')} to ${r.target} after each merge.` : `The export to ${r.target} is off.` }
543
+ case 'seam_export_remote': { const r = await client.setExport(needQuilt(), { target: args.target, lines: args.lines, releases: args.releases, credential: args.credential, scratch: args.scratch, enabled: args.enabled !== false, lfs: args.lfs, main_as: args.main_as, push: args.push, exclude: args.exclude }); return r.enabled ? `Exporting ${r.lines.join(', ')} to ${r.target} ${r.push === 'tag' ? 'up to the newest tagged patch' : 'after each merge'}.` : `The export to ${r.target} is off.` }
544
+ case 'seam_export_push': { const r = await client.exportPush(needQuilt()); return `Pushing to ${r.target} now; seam_export_status shows how it went.` }
534
545
  case 'seam_export_archive': {
535
546
  if (hosted) return `This server cannot write files to your computer. Fetch the archive over HTTP with your token: POST /v1/quilts/${encodeURIComponent(needQuilt())}/exit/archive`
536
547
  const r = await client.exitArchive(needQuilt())
@@ -548,7 +559,7 @@ export async function runSeamTool (name, args, { client, found = null, hosted =
548
559
  writeFileSync(file, r.bytes)
549
560
  return `Wrote ${args.rev || 'main'} (${String(r.commit || 'empty').slice(0, 12)}) to ${file}: ${r.bytes.length} bytes.`
550
561
  }
551
- case 'seam_export_status': { const r = await client.exportStatus(needQuilt()); return JSON.stringify({ target: r.target, lines: r.lines, releases: r.releases, enabled: r.enabled, status: r.status }, null, 2) }
562
+ case 'seam_export_status': { const r = await client.exportStatus(needQuilt()); return JSON.stringify({ target: r.target, lines: r.lines, main_as: r.main_as, push: r.push, exclude: r.exclude, releases: r.releases, enabled: r.enabled, status: r.status }, null, 2) }
552
563
  case 'seam_import_cutover': { const r = await client.cutover(args.id, { credential: args.credential }); return `Cut over: import ${r.import.id} is done; main exports to ${r.export.target} after each merge.` }
553
564
  case 'seam_import_status': return describeImportRecord(await client.importStatus(args.id))
554
565
  case 'seam_import_authors': { const r = await client.answerImportAuthors(args.id, { authors: args.authors }); return `Import ${r.id} has its authors and runs now.` }
@@ -591,12 +602,9 @@ export async function runSeamTool (name, args, { client, found = null, hosted =
591
602
  }
592
603
  case 'seam_pull': {
593
604
  if (!found) throw new Error('seam_pull works on a block folder: run seam mcp in a folder made by seam clone or seam checkout.')
605
+ const was = { ...block }
594
606
  const r = await pull(client, found.dir, block)
595
- const kept = describeBoth(r, { diff: 'seam_diff', resolve: 'seam_resolve (choose ours or theirs)' })
596
- if (r.noop) return block.square ? `Already up to date: ${block.square_short || block.square} sits on ${block.line}'s tip.` : `Already up to date with ${block.line}.`
597
- if (r.left) return `${r.left.square} was ${r.left.state}; the folder is now on ${r.line} (${r.at.slice(0, 12)}, ${r.written} file(s) updated).` + kept
598
- if (r.rebased) return describeWrite(r.result, 'Rebased') + `\n${r.written} file(s) updated in the folder.` + kept
599
- return `Pulled ${r.line} (now ${r.at.slice(0, 12)}): ${r.written} file(s) updated.` + kept
607
+ return describePull(r, was, { diff: 'seam_diff', resolve: 'seam_resolve (choose ours or theirs)' })
600
608
  }
601
609
  case 'seam_block_rebuild': {
602
610
  if (!found) throw new Error('seam_block_rebuild works on a block folder: run seam mcp in a folder made by seam checkout.')
@@ -638,6 +646,10 @@ export async function runSeamTool (name, args, { client, found = null, hosted =
638
646
  return describePolicyVersions(r)
639
647
  }
640
648
  case 'seam_orgs': return args.org ? describeOrg(await client.org(args.org)) : describeOrgs(await client.orgs())
649
+ case 'seam_quilt_delete': return describeDeletedQuilt(await client.deleteQuilt(needQuilt(), args.confirm))
650
+ case 'seam_quilts_deleted': return describeDeletedQuilts(await client.deletedQuilts({ org: args.org }), { org: args.org })
651
+ case 'seam_quilt_restore': { const r = await client.undeleteQuilt(args.quilt, { org: args.org }); return `Restored ${r.path || r.quilt}: it is back as it was. The squares the delete closed stay closed; seam_reopen brings one back.` }
652
+ case 'seam_account_log': return describePersonLog(await client.personLog({ limit: args.limit, before: args.before }))
641
653
  case 'seam_org_log': return describeOrgLog(await client.orgLog(args.org, { limit: args.limit, before: args.before }))
642
654
  case 'seam_apps': return describeApps(await client.apps({ verified: !!args.verified }))
643
655
  case 'seam_app': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyquilt/seam",
3
- "version": "0.143.0",
3
+ "version": "0.145.0",
4
4
  "description": "seam: Seam's command. Git's verbs for source control that runs on the server, built for many agents and people at once.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",