@try-works/dsh-memory 0.1.18 → 0.1.20
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/LICENSE +21 -21
- package/README.md +585 -585
- package/STATUS.md +287 -287
- package/cordis.patch.yml +69 -69
- package/package.json +1 -1
- package/scripts/check-package.mjs +76 -76
- package/src/SYSTEM.template.md +311 -311
- package/src/capture.js +242 -242
- package/src/client/index.js +886 -886
- package/src/config.js +57 -57
- package/src/consolidate-run.js +132 -132
- package/src/consolidate.js +330 -330
- package/src/context.js +88 -88
- package/src/decisions.js +106 -106
- package/src/defaults.js +93 -93
- package/src/digest.js +255 -255
- package/src/entry.js +179 -179
- package/src/eval.js +72 -72
- package/src/files.js +60 -60
- package/src/fixture.js +56 -56
- package/src/forget.js +71 -71
- package/src/freshness.js +65 -65
- package/src/fts.js +166 -166
- package/src/git.js +53 -53
- package/src/history.js +226 -226
- package/src/host.js +866 -857
- package/src/index.js +115 -94
- package/src/inject.js +108 -108
- package/src/layers.js +178 -178
- package/src/ledger.js +19 -19
- package/src/map.js +184 -184
- package/src/models.js +136 -136
- package/src/queue.js +602 -602
- package/src/rank.js +31 -31
- package/src/recover.js +91 -91
- package/src/refresh.js +80 -80
- package/src/remember.js +58 -58
- package/src/rerank.js +90 -90
- package/src/search.js +75 -75
- package/src/select.js +57 -57
- package/src/session.js +60 -60
- package/src/state.js +240 -240
- package/src/store.js +128 -128
- package/src/summarize.js +195 -195
- package/src/system-doc.js +171 -171
- package/src/tasks.js +617 -617
- package/src/telemetry.js +122 -122
- package/src/tools.js +828 -818
- package/src/working.js +288 -288
- package/src/workspace-key.js +68 -68
- package/src/workspace.js +87 -87
package/src/workspace-key.js
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
// Stable, portable workspace-key derivation (pure). A workspace key must be
|
|
2
|
-
// identical across devices that check out the same memory repo, even though the
|
|
3
|
-
// local folder path differs. Resolution order: explicit > git remote URL > basename.
|
|
4
|
-
|
|
5
|
-
/** Normalize any git remote spelling to a lowercase https URL with no trailing .git or slash. */
|
|
6
|
-
export function normalizeRemoteUrl(url) {
|
|
7
|
-
if (typeof url !== 'string' || url.trim() === '') return undefined
|
|
8
|
-
let u = url.trim()
|
|
9
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) {
|
|
10
|
-
u = u.replace(/^[a-z][a-z0-9+.-]*:\/\//i, 'https://')
|
|
11
|
-
u = u.replace(/^https:\/\/[^/@]+@/, 'https://') // strip userinfo
|
|
12
|
-
} else if (/^[^/\\:@]+@[^/\\:]+:.+/.test(u)) {
|
|
13
|
-
const i = u.indexOf(':')
|
|
14
|
-
u = 'https://' + u.slice(0, i).replace(/^[^@]+@/, '') + '/' + u.slice(i + 1)
|
|
15
|
-
}
|
|
16
|
-
u = u.replace(/\.git$/i, '')
|
|
17
|
-
u = u.replace(/\/+$/, '')
|
|
18
|
-
return u.toLowerCase()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Filesystem-safe slug from a display segment.
|
|
23
|
-
*
|
|
24
|
-
* Keeps Unicode letters and digits, replaces everything else with a hyphen. A Latin-only rule
|
|
25
|
-
* (a-z0-9) was actively harmful: any CJK, Cyrillic or Greek topic slugged to an EMPTY string,
|
|
26
|
-
* so every such topic collapsed into one file and silently merged unrelated subjects. Keeping
|
|
27
|
-
* the script is the only lossless option - and pure-ASCII input is unaffected, so existing
|
|
28
|
-
* slugs and file names do not move.
|
|
29
|
-
*
|
|
30
|
-
* Bounded on code points, not UTF-16 units, so a multi-byte name is never cut mid-character.
|
|
31
|
-
*/
|
|
32
|
-
export function slugify(segment) {
|
|
33
|
-
const raw = String(segment ?? '').trim().toLowerCase()
|
|
34
|
-
const slug = raw.replace(/[^\p{L}\p{N}]+/gu, '-').replace(/^-+|-+$/g, '')
|
|
35
|
-
if (slug === '') return ''
|
|
36
|
-
const points = [...slug]
|
|
37
|
-
return points.length > 60 ? points.slice(0, 60).join('').replace(/-+$/g, '') : slug
|
|
38
|
-
}
|
|
39
|
-
/** Reject traversal / empty / oversized keys. */
|
|
40
|
-
export function sanitizeKey(key) {
|
|
41
|
-
if (typeof key !== 'string') return undefined
|
|
42
|
-
const s = slugify(key)
|
|
43
|
-
if (!s || s === '.' || s === '..' || s.length > 120) return undefined
|
|
44
|
-
return s
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Derive a key from a normalized remote URL; GitHub drops the host, others keep it. */
|
|
48
|
-
export function keyFromRemote(url) {
|
|
49
|
-
const n = normalizeRemoteUrl(url)
|
|
50
|
-
if (!n) return undefined
|
|
51
|
-
try {
|
|
52
|
-
const u = new URL(n)
|
|
53
|
-
const p = u.pathname.replace(/^\/+|\/+$/g, '')
|
|
54
|
-
if (u.hostname === 'github.com') return slugify(p.split('/').join('-'))
|
|
55
|
-
return slugify(u.hostname.replace(/^www\./, '') + '-' + p.split('/').join('-'))
|
|
56
|
-
} catch {
|
|
57
|
-
return undefined
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Full resolution order: explicit -> remote -> basename. */
|
|
62
|
-
export function deriveWorkspaceKey({ remote, basename, explicit } = {}) {
|
|
63
|
-
if (typeof explicit === 'string' && explicit.trim() !== '') return sanitizeKey(explicit)
|
|
64
|
-
const fromRemote = keyFromRemote(remote)
|
|
65
|
-
if (fromRemote) return fromRemote
|
|
66
|
-
if (typeof basename === 'string' && basename.trim() !== '') return sanitizeKey(basename)
|
|
67
|
-
return undefined
|
|
68
|
-
}
|
|
1
|
+
// Stable, portable workspace-key derivation (pure). A workspace key must be
|
|
2
|
+
// identical across devices that check out the same memory repo, even though the
|
|
3
|
+
// local folder path differs. Resolution order: explicit > git remote URL > basename.
|
|
4
|
+
|
|
5
|
+
/** Normalize any git remote spelling to a lowercase https URL with no trailing .git or slash. */
|
|
6
|
+
export function normalizeRemoteUrl(url) {
|
|
7
|
+
if (typeof url !== 'string' || url.trim() === '') return undefined
|
|
8
|
+
let u = url.trim()
|
|
9
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) {
|
|
10
|
+
u = u.replace(/^[a-z][a-z0-9+.-]*:\/\//i, 'https://')
|
|
11
|
+
u = u.replace(/^https:\/\/[^/@]+@/, 'https://') // strip userinfo
|
|
12
|
+
} else if (/^[^/\\:@]+@[^/\\:]+:.+/.test(u)) {
|
|
13
|
+
const i = u.indexOf(':')
|
|
14
|
+
u = 'https://' + u.slice(0, i).replace(/^[^@]+@/, '') + '/' + u.slice(i + 1)
|
|
15
|
+
}
|
|
16
|
+
u = u.replace(/\.git$/i, '')
|
|
17
|
+
u = u.replace(/\/+$/, '')
|
|
18
|
+
return u.toLowerCase()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Filesystem-safe slug from a display segment.
|
|
23
|
+
*
|
|
24
|
+
* Keeps Unicode letters and digits, replaces everything else with a hyphen. A Latin-only rule
|
|
25
|
+
* (a-z0-9) was actively harmful: any CJK, Cyrillic or Greek topic slugged to an EMPTY string,
|
|
26
|
+
* so every such topic collapsed into one file and silently merged unrelated subjects. Keeping
|
|
27
|
+
* the script is the only lossless option - and pure-ASCII input is unaffected, so existing
|
|
28
|
+
* slugs and file names do not move.
|
|
29
|
+
*
|
|
30
|
+
* Bounded on code points, not UTF-16 units, so a multi-byte name is never cut mid-character.
|
|
31
|
+
*/
|
|
32
|
+
export function slugify(segment) {
|
|
33
|
+
const raw = String(segment ?? '').trim().toLowerCase()
|
|
34
|
+
const slug = raw.replace(/[^\p{L}\p{N}]+/gu, '-').replace(/^-+|-+$/g, '')
|
|
35
|
+
if (slug === '') return ''
|
|
36
|
+
const points = [...slug]
|
|
37
|
+
return points.length > 60 ? points.slice(0, 60).join('').replace(/-+$/g, '') : slug
|
|
38
|
+
}
|
|
39
|
+
/** Reject traversal / empty / oversized keys. */
|
|
40
|
+
export function sanitizeKey(key) {
|
|
41
|
+
if (typeof key !== 'string') return undefined
|
|
42
|
+
const s = slugify(key)
|
|
43
|
+
if (!s || s === '.' || s === '..' || s.length > 120) return undefined
|
|
44
|
+
return s
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Derive a key from a normalized remote URL; GitHub drops the host, others keep it. */
|
|
48
|
+
export function keyFromRemote(url) {
|
|
49
|
+
const n = normalizeRemoteUrl(url)
|
|
50
|
+
if (!n) return undefined
|
|
51
|
+
try {
|
|
52
|
+
const u = new URL(n)
|
|
53
|
+
const p = u.pathname.replace(/^\/+|\/+$/g, '')
|
|
54
|
+
if (u.hostname === 'github.com') return slugify(p.split('/').join('-'))
|
|
55
|
+
return slugify(u.hostname.replace(/^www\./, '') + '-' + p.split('/').join('-'))
|
|
56
|
+
} catch {
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Full resolution order: explicit -> remote -> basename. */
|
|
62
|
+
export function deriveWorkspaceKey({ remote, basename, explicit } = {}) {
|
|
63
|
+
if (typeof explicit === 'string' && explicit.trim() !== '') return sanitizeKey(explicit)
|
|
64
|
+
const fromRemote = keyFromRemote(remote)
|
|
65
|
+
if (fromRemote) return fromRemote
|
|
66
|
+
if (typeof basename === 'string' && basename.trim() !== '') return sanitizeKey(basename)
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
package/src/workspace.js
CHANGED
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
// Workspace association: a portable registry (memory.json, committed) maps
|
|
2
|
-
// key -> {title, remotes, createdAt}; a device-local map (state/associations.json,
|
|
3
|
-
// gitignored) maps key -> {path, workspaceId}. Paths are machine-specific and never
|
|
4
|
-
// committed, so the same store reconciles across devices by remote URL or basename.
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
6
|
-
import { basename, join } from 'node:path'
|
|
7
|
-
import { workspacesDir, workspaceDir } from './store.js'
|
|
8
|
-
import { deriveWorkspaceKey, sanitizeKey } from './workspace-key.js'
|
|
9
|
-
|
|
10
|
-
export function associationsPath(root) { return join(root, 'state', 'associations.json') }
|
|
11
|
-
|
|
12
|
-
export function readAssociations(root) {
|
|
13
|
-
try { return JSON.parse(readFileSync(associationsPath(root), 'utf8')) }
|
|
14
|
-
catch { return {} }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function writeAssociations(root, value) {
|
|
18
|
-
mkdirSync(join(root, 'state'), { recursive: true })
|
|
19
|
-
writeFileSync(associationsPath(root), JSON.stringify(value, null, 2) + '\n')
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Resolution order: explicit > device-local association (by canonical path) > remote > basename. */
|
|
23
|
-
export function resolveKeyFor(root, { cwd, remote, basename, explicit } = {}) {
|
|
24
|
-
if (explicit) return sanitizeKey(explicit)
|
|
25
|
-
if (cwd) {
|
|
26
|
-
const assoc = readAssociations(root)
|
|
27
|
-
const hit = Object.entries(assoc).find(([, a]) => a.path === cwd)
|
|
28
|
-
if (hit) return hit[0]
|
|
29
|
-
}
|
|
30
|
-
return deriveWorkspaceKey({ remote, basename })
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Registry + on-disk dirs + device-local associations, merged. */
|
|
34
|
-
export function listWorkspaces(store) {
|
|
35
|
-
const mem = store.readMemoryJson()
|
|
36
|
-
const registry = mem.workspaces ?? {}
|
|
37
|
-
const assoc = readAssociations(store.root)
|
|
38
|
-
const keys = new Set(Object.keys(registry))
|
|
39
|
-
const wdir = workspacesDir(store.root)
|
|
40
|
-
if (existsSync(wdir)) {
|
|
41
|
-
for (const name of readdirSync(wdir)) {
|
|
42
|
-
if (existsSync(join(wdir, name, 'meta.json'))) keys.add(name)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return [...keys].sort().map((key) => {
|
|
46
|
-
const rec = registry[key] ?? {}
|
|
47
|
-
return {
|
|
48
|
-
key,
|
|
49
|
-
title: rec.title ?? key,
|
|
50
|
-
remotes: rec.remotes ?? [],
|
|
51
|
-
createdAt: rec.createdAt,
|
|
52
|
-
path: assoc[key]?.path,
|
|
53
|
-
workspaceId: assoc[key]?.workspaceId,
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Associate a key (explicit, or derived from remote/basename) and record it durably. */
|
|
59
|
-
export function associate(store, { key, title, remote, cwd, workspaceId } = {}) {
|
|
60
|
-
const safe = sanitizeKey(key) ?? deriveWorkspaceKey({ remote, basename: cwd ? basename(cwd) : undefined })
|
|
61
|
-
if (!safe) throw new Error('cannot derive a workspace key; pass an explicit key')
|
|
62
|
-
store.ensureWorkspace(safe, { title: title ?? safe, remotes: remote ? [remote] : [] })
|
|
63
|
-
|
|
64
|
-
// portable registry (committed to git)
|
|
65
|
-
const mem = store.readMemoryJson()
|
|
66
|
-
mem.workspaces = mem.workspaces ?? {}
|
|
67
|
-
const prev = mem.workspaces[safe] ?? {}
|
|
68
|
-
mem.workspaces[safe] = {
|
|
69
|
-
title: title ?? prev.title ?? safe,
|
|
70
|
-
remotes: remote ? Array.from(new Set([...(prev.remotes ?? []), remote])) : (prev.remotes ?? []),
|
|
71
|
-
createdAt: prev.createdAt ?? new Date().toISOString(),
|
|
72
|
-
}
|
|
73
|
-
store.writeMemoryJson(mem)
|
|
74
|
-
|
|
75
|
-
// keep the per-workspace meta.json in sync with the registry
|
|
76
|
-
writeFileSync(join(workspaceDir(store.root, safe), 'meta.json'), JSON.stringify({
|
|
77
|
-
key: safe, title: mem.workspaces[safe].title, remotes: mem.workspaces[safe].remotes, createdAt: mem.workspaces[safe].createdAt,
|
|
78
|
-
}, null, 2) + '\n')
|
|
79
|
-
|
|
80
|
-
// device-local association (gitignored)
|
|
81
|
-
if (cwd || workspaceId) {
|
|
82
|
-
const assoc = readAssociations(store.root)
|
|
83
|
-
assoc[safe] = { ...(assoc[safe] ?? {}), ...(cwd ? { path: cwd } : {}), ...(workspaceId ? { workspaceId } : {}) }
|
|
84
|
-
writeAssociations(store.root, assoc)
|
|
85
|
-
}
|
|
86
|
-
return safe
|
|
87
|
-
}
|
|
1
|
+
// Workspace association: a portable registry (memory.json, committed) maps
|
|
2
|
+
// key -> {title, remotes, createdAt}; a device-local map (state/associations.json,
|
|
3
|
+
// gitignored) maps key -> {path, workspaceId}. Paths are machine-specific and never
|
|
4
|
+
// committed, so the same store reconciles across devices by remote URL or basename.
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { basename, join } from 'node:path'
|
|
7
|
+
import { workspacesDir, workspaceDir } from './store.js'
|
|
8
|
+
import { deriveWorkspaceKey, sanitizeKey } from './workspace-key.js'
|
|
9
|
+
|
|
10
|
+
export function associationsPath(root) { return join(root, 'state', 'associations.json') }
|
|
11
|
+
|
|
12
|
+
export function readAssociations(root) {
|
|
13
|
+
try { return JSON.parse(readFileSync(associationsPath(root), 'utf8')) }
|
|
14
|
+
catch { return {} }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function writeAssociations(root, value) {
|
|
18
|
+
mkdirSync(join(root, 'state'), { recursive: true })
|
|
19
|
+
writeFileSync(associationsPath(root), JSON.stringify(value, null, 2) + '\n')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Resolution order: explicit > device-local association (by canonical path) > remote > basename. */
|
|
23
|
+
export function resolveKeyFor(root, { cwd, remote, basename, explicit } = {}) {
|
|
24
|
+
if (explicit) return sanitizeKey(explicit)
|
|
25
|
+
if (cwd) {
|
|
26
|
+
const assoc = readAssociations(root)
|
|
27
|
+
const hit = Object.entries(assoc).find(([, a]) => a.path === cwd)
|
|
28
|
+
if (hit) return hit[0]
|
|
29
|
+
}
|
|
30
|
+
return deriveWorkspaceKey({ remote, basename })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Registry + on-disk dirs + device-local associations, merged. */
|
|
34
|
+
export function listWorkspaces(store) {
|
|
35
|
+
const mem = store.readMemoryJson()
|
|
36
|
+
const registry = mem.workspaces ?? {}
|
|
37
|
+
const assoc = readAssociations(store.root)
|
|
38
|
+
const keys = new Set(Object.keys(registry))
|
|
39
|
+
const wdir = workspacesDir(store.root)
|
|
40
|
+
if (existsSync(wdir)) {
|
|
41
|
+
for (const name of readdirSync(wdir)) {
|
|
42
|
+
if (existsSync(join(wdir, name, 'meta.json'))) keys.add(name)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...keys].sort().map((key) => {
|
|
46
|
+
const rec = registry[key] ?? {}
|
|
47
|
+
return {
|
|
48
|
+
key,
|
|
49
|
+
title: rec.title ?? key,
|
|
50
|
+
remotes: rec.remotes ?? [],
|
|
51
|
+
createdAt: rec.createdAt,
|
|
52
|
+
path: assoc[key]?.path,
|
|
53
|
+
workspaceId: assoc[key]?.workspaceId,
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Associate a key (explicit, or derived from remote/basename) and record it durably. */
|
|
59
|
+
export function associate(store, { key, title, remote, cwd, workspaceId } = {}) {
|
|
60
|
+
const safe = sanitizeKey(key) ?? deriveWorkspaceKey({ remote, basename: cwd ? basename(cwd) : undefined })
|
|
61
|
+
if (!safe) throw new Error('cannot derive a workspace key; pass an explicit key')
|
|
62
|
+
store.ensureWorkspace(safe, { title: title ?? safe, remotes: remote ? [remote] : [] })
|
|
63
|
+
|
|
64
|
+
// portable registry (committed to git)
|
|
65
|
+
const mem = store.readMemoryJson()
|
|
66
|
+
mem.workspaces = mem.workspaces ?? {}
|
|
67
|
+
const prev = mem.workspaces[safe] ?? {}
|
|
68
|
+
mem.workspaces[safe] = {
|
|
69
|
+
title: title ?? prev.title ?? safe,
|
|
70
|
+
remotes: remote ? Array.from(new Set([...(prev.remotes ?? []), remote])) : (prev.remotes ?? []),
|
|
71
|
+
createdAt: prev.createdAt ?? new Date().toISOString(),
|
|
72
|
+
}
|
|
73
|
+
store.writeMemoryJson(mem)
|
|
74
|
+
|
|
75
|
+
// keep the per-workspace meta.json in sync with the registry
|
|
76
|
+
writeFileSync(join(workspaceDir(store.root, safe), 'meta.json'), JSON.stringify({
|
|
77
|
+
key: safe, title: mem.workspaces[safe].title, remotes: mem.workspaces[safe].remotes, createdAt: mem.workspaces[safe].createdAt,
|
|
78
|
+
}, null, 2) + '\n')
|
|
79
|
+
|
|
80
|
+
// device-local association (gitignored)
|
|
81
|
+
if (cwd || workspaceId) {
|
|
82
|
+
const assoc = readAssociations(store.root)
|
|
83
|
+
assoc[safe] = { ...(assoc[safe] ?? {}), ...(cwd ? { path: cwd } : {}), ...(workspaceId ? { workspaceId } : {}) }
|
|
84
|
+
writeAssociations(store.root, assoc)
|
|
85
|
+
}
|
|
86
|
+
return safe
|
|
87
|
+
}
|