@agimon-ai/doompi-hashline 0.0.1-alpha.1
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 +22 -0
- package/README.md +67 -0
- package/dist/adapters/node/files.cjs +2 -0
- package/dist/adapters/node/files.cjs.map +1 -0
- package/dist/adapters/node/files.d.cts +12 -0
- package/dist/adapters/node/files.d.cts.map +1 -0
- package/dist/adapters/node/files.d.mts +12 -0
- package/dist/adapters/node/files.d.mts.map +1 -0
- package/dist/adapters/node/files.mjs +2 -0
- package/dist/adapters/node/files.mjs.map +1 -0
- package/dist/files.cjs +1 -0
- package/dist/files.d.cts +2 -0
- package/dist/files.d.mts +2 -0
- package/dist/files.mjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +1 -0
- package/dist/services/hashline.cjs +11 -0
- package/dist/services/hashline.cjs.map +1 -0
- package/dist/services/hashline.d.cts +21 -0
- package/dist/services/hashline.d.cts.map +1 -0
- package/dist/services/hashline.d.mts +21 -0
- package/dist/services/hashline.d.mts.map +1 -0
- package/dist/services/hashline.mjs +11 -0
- package/dist/services/hashline.mjs.map +1 -0
- package/dist/types/hashline.d.cts +34 -0
- package/dist/types/hashline.d.cts.map +1 -0
- package/dist/types/hashline.d.mts +34 -0
- package/dist/types/hashline.d.mts.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vuong Ngo
|
|
4
|
+
Copyright (c) 2026 pulseaiclub
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @agimon-ai/doompi-hashline
|
|
2
|
+
|
|
3
|
+
Shared snapshot-bound file tags and line anchors for DoomPi tools.
|
|
4
|
+
|
|
5
|
+
This is a foundation library, not a Pi extension. It has no `pi.extensions` entry, Cordis service,
|
|
6
|
+
or Pi peer dependency. Tool packages use the same protocol implementation without importing one
|
|
7
|
+
another.
|
|
8
|
+
|
|
9
|
+
## Protocol
|
|
10
|
+
|
|
11
|
+
The root export contains pure string and edit operations:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { formatFileHeader, formatTaggedLine, parseFileHeader, parseTaggedLine } from '@agimon-ai/doompi-hashline';
|
|
15
|
+
|
|
16
|
+
const header = formatFileHeader('src/example.ts', 'A1b2C3d4');
|
|
17
|
+
const line = formatTaggedLine('const value = 1;', 1);
|
|
18
|
+
|
|
19
|
+
parseFileHeader(header);
|
|
20
|
+
parseTaggedLine(line);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
File headers use an eight-character base64url SHA-256 prefix of the exact file bytes. Tagged lines
|
|
24
|
+
use a three-letter, whitespace-insensitive FNV-1a64 anchor. Match rows start with `>> `, and context
|
|
25
|
+
rows start with three spaces.
|
|
26
|
+
|
|
27
|
+
The root also exports anchor parsing, newline normalization, and pure original-snapshot range
|
|
28
|
+
application.
|
|
29
|
+
|
|
30
|
+
## File adapters
|
|
31
|
+
|
|
32
|
+
Node-specific helpers are isolated at the `/files` subpath:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import {
|
|
36
|
+
computeFileTag,
|
|
37
|
+
decodeUtf8,
|
|
38
|
+
displayPath,
|
|
39
|
+
isWritableFile,
|
|
40
|
+
resolveInputPath,
|
|
41
|
+
resolveReadInputPath,
|
|
42
|
+
} from '@agimon-ai/doompi-hashline/files';
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`decodeUtf8` rejects invalid UTF-8. `isWritableFile` gates anchors to content the
|
|
46
|
+
agent can edit. `resolveInputPath`, `resolveReadInputPath`, and `displayPath`
|
|
47
|
+
preserve Pi-compatible path behavior across the read, grep, and edit tools.
|
|
48
|
+
|
|
49
|
+
## Attribution
|
|
50
|
+
|
|
51
|
+
The hashline protocol and FNV anchor approach are adapted from
|
|
52
|
+
[Phi's writetool](https://github.com/pulseaiclub/phi/tree/main/internal/tools/writetool), licensed
|
|
53
|
+
under MIT by pulseaiclub. DoomPi uses an exact-byte SHA-256 file tag as an intentional compatibility
|
|
54
|
+
boundary.
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pnpm build
|
|
60
|
+
pnpm typecheck
|
|
61
|
+
pnpm test
|
|
62
|
+
pnpm lint
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require("node:crypto"),t=require("node:fs"),n=require("node:fs/promises"),r=require("node:os"),i=require("node:path"),a=require("node:url");const o=/[\u00A0\u2000-\u200A\u202F\u205F\u3000]/gu;function s(t){return(0,e.createHash)(`sha256`).update(t).digest(`base64url`).slice(0,8)}function c(e,t){try{return new TextDecoder(`utf-8`,{fatal:!0}).decode(e)}catch(e){throw Error(`Cannot use hashline tools on ${t}: the file is not valid UTF-8.`,{cause:e})}}async function l(e){return(0,n.access)(e,t.constants.W_OK).then(()=>!0,()=>!1)}function u(e,t){let n=e.replace(o,` `);return n.startsWith(`@`)&&(n=n.slice(1)),process.platform===`win32`&&(n=m(n)),n===`~`?n=(0,r.homedir)():n.startsWith(`~${i.sep}`)?n=(0,i.resolve)((0,r.homedir)(),n.slice(2)):n.startsWith(`file://`)&&(n=(0,a.fileURLToPath)(n)),(0,i.isAbsolute)(n)?(0,i.resolve)(n):(0,i.resolve)(t,n)}async function d(e,t){let n=u(e,t);if(await p(n))return n;let r=[n.replace(/ (AM|PM)\./giu,` $1.`),n.normalize(`NFD`),n.replaceAll(`'`,`’`),n.normalize(`NFD`).replaceAll(`'`,`’`)];for(let e of r)if(e!==n&&await p(e))return e;return n}function f(e,t){let n=(0,i.relative)(t,e),r=(n!==``&&n!==`..`&&!n.startsWith(`..${i.sep}`)&&!(0,i.isAbsolute)(n)?n:e).split(i.sep).join(`/`);return r===`~`||r.startsWith(`~/`)?`./${r}`:r}async function p(e){return(0,n.access)(e).then(()=>!0,()=>!1)}function m(e){if(!e.startsWith(`/`)||e.startsWith(`//`)||e.includes(`\\`))return e;let t=/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/iu.exec(e);if(!t?.[1])return e;let n=t[2]?.replaceAll(`/`,`\\`);return`${t[1].toUpperCase()}:\\${n??``}`}exports.computeFileTag=s,exports.decodeUtf8=c,exports.displayPath=f,exports.isWritableFile=l,exports.resolveInputPath=u,exports.resolveReadInputPath=d;
|
|
2
|
+
//# sourceMappingURL=files.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.cjs","names":["createHash","access","constants","homedir","sep","resolve","fileURLToPath","isAbsolute","relative"],"sources":["../../../src/adapters/node/files.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { constants } from 'node:fs';\nimport { access } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { isAbsolute, relative, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/gu;\nconst NARROW_NO_BREAK_SPACE = '\\u202f';\n\nexport function computeFileTag(bytes: Uint8Array): string {\n return createHash('sha256').update(bytes).digest('base64url').slice(0, 8);\n}\n\nexport function decodeUtf8(bytes: Uint8Array, path: string): string {\n try {\n return new TextDecoder('utf-8', { fatal: true }).decode(bytes);\n } catch (error) {\n throw new Error(`Cannot use hashline tools on ${path}: the file is not valid UTF-8.`, { cause: error });\n }\n}\n\n/** Return whether the current process can edit an existing file. */\nexport async function isWritableFile(path: string): Promise<boolean> {\n return access(path, constants.W_OK).then(\n () => true,\n () => false,\n );\n}\n\nexport function resolveInputPath(input: string, cwd: string): string {\n let path = input.replace(UNICODE_SPACES, ' ');\n if (path.startsWith('@')) path = path.slice(1);\n if (process.platform === 'win32') path = normalizeWindowsShellPath(path);\n if (path === '~') path = homedir();\n else if (path.startsWith(`~${sep}`)) path = resolve(homedir(), path.slice(2));\n else if (path.startsWith('file://')) path = fileURLToPath(path);\n return isAbsolute(path) ? resolve(path) : resolve(cwd, path);\n}\n\n/** Resolve the same existing-file variants as Pi's native read tool. */\nexport async function resolveReadInputPath(input: string, cwd: string): Promise<string> {\n const resolved = resolveInputPath(input, cwd);\n if (await pathExists(resolved)) return resolved;\n\n const variants = [\n resolved.replace(/ (AM|PM)\\./giu, `${NARROW_NO_BREAK_SPACE}$1.`),\n resolved.normalize('NFD'),\n resolved.replaceAll(\"'\", '\\u2019'),\n resolved.normalize('NFD').replaceAll(\"'\", '\\u2019'),\n ];\n for (const candidate of variants) {\n if (candidate !== resolved && (await pathExists(candidate))) return candidate;\n }\n return resolved;\n}\n\nexport function displayPath(absolutePath: string, cwd: string): string {\n const path = relative(cwd, absolutePath);\n const display =\n path !== '' && path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path) ? path : absolutePath;\n const portable = display.split(sep).join('/');\n return portable === '~' || portable.startsWith('~/') ? `./${portable}` : portable;\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n return access(path).then(\n () => true,\n () => false,\n );\n}\n\nfunction normalizeWindowsShellPath(path: string): string {\n if (!path.startsWith('/') || path.startsWith('//') || path.includes('\\\\')) return path;\n const match = /^\\/(?:mnt\\/|cygdrive\\/)?([a-z])(?:\\/(.*))?$/iu.exec(path);\n if (!match?.[1]) return path;\n const suffix = match[2]?.replaceAll('/', '\\\\');\n return `${match[1].toUpperCase()}:\\\\${suffix ?? ''}`;\n}\n"],"mappings":"kJAOA,MAAM,EAAiB,4CAGvB,SAAgB,EAAe,EAA2B,CACxD,OAAA,EAAOA,EAAAA,WAAAA,CAAW,QAAQ,CAAC,CAAC,OAAO,CAAK,CAAC,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,EAAG,CAAC,CAC1E,CAEA,SAAgB,EAAW,EAAmB,EAAsB,CAClE,GAAI,CACF,OAAO,IAAI,YAAY,QAAS,CAAE,MAAO,EAAK,CAAC,CAAC,CAAC,OAAO,CAAK,CAC/D,OAAS,EAAO,CACd,MAAU,MAAM,gCAAgC,EAAK,gCAAiC,CAAE,MAAO,CAAM,CAAC,CACxG,CACF,CAGA,eAAsB,EAAe,EAAgC,CACnE,OAAA,EAAOC,EAAAA,OAAAA,CAAO,EAAMC,EAAAA,UAAU,IAAI,CAAC,CAAC,SAC5B,OACA,EACR,CACF,CAEA,SAAgB,EAAiB,EAAe,EAAqB,CACnE,IAAI,EAAO,EAAM,QAAQ,EAAgB,GAAG,EAM5C,OALI,EAAK,WAAW,GAAG,IAAG,EAAO,EAAK,MAAM,CAAC,GACzC,QAAQ,WAAa,UAAS,EAAO,EAA0B,CAAI,GACnE,IAAS,IAAK,GAAA,EAAOC,EAAAA,QAAAA,CAAQ,EACxB,EAAK,WAAW,IAAIC,EAAAA,KAAK,EAAG,GAAA,EAAOC,EAAAA,QAAAA,EAAAA,EAAQF,EAAAA,QAAAA,CAAQ,EAAG,EAAK,MAAM,CAAC,CAAC,EACnE,EAAK,WAAW,SAAS,IAAG,GAAA,EAAOG,EAAAA,cAAAA,CAAc,CAAI,IAC9D,EAAOC,EAAAA,WAAAA,CAAW,CAAI,GAAA,EAAIF,EAAAA,QAAAA,CAAQ,CAAI,GAAA,EAAIA,EAAAA,QAAAA,CAAQ,EAAK,CAAI,CAC7D,CAGA,eAAsB,EAAqB,EAAe,EAA8B,CACtF,IAAM,EAAW,EAAiB,EAAO,CAAG,EAC5C,GAAI,MAAM,EAAW,CAAQ,EAAG,OAAO,EAEvC,IAAM,EAAW,CACf,EAAS,QAAQ,gBAAiB,MAA6B,EAC/D,EAAS,UAAU,KAAK,EACxB,EAAS,WAAW,IAAK,GAAQ,EACjC,EAAS,UAAU,KAAK,CAAC,CAAC,WAAW,IAAK,GAAQ,CACpD,EACA,IAAK,IAAM,KAAa,EACtB,GAAI,IAAc,GAAa,MAAM,EAAW,CAAS,EAAI,OAAO,EAEtE,OAAO,CACT,CAEA,SAAgB,EAAY,EAAsB,EAAqB,CACrE,IAAM,GAAA,EAAOG,EAAAA,SAAAA,CAAS,EAAK,CAAY,EAGjC,GADJ,IAAS,IAAM,IAAS,MAAQ,CAAC,EAAK,WAAW,KAAKJ,EAAAA,KAAK,GAAK,EAAA,EAACG,EAAAA,WAAAA,CAAW,CAAI,EAAI,EAAO,EAAA,CACpE,MAAMH,EAAAA,GAAG,CAAC,CAAC,KAAK,GAAG,EAC5C,OAAO,IAAa,KAAO,EAAS,WAAW,IAAI,EAAI,KAAK,IAAa,CAC3E,CAEA,eAAe,EAAW,EAAgC,CACxD,OAAA,EAAOH,EAAAA,OAAAA,CAAO,CAAI,CAAC,CAAC,SACZ,OACA,EACR,CACF,CAEA,SAAS,EAA0B,EAAsB,CACvD,GAAI,CAAC,EAAK,WAAW,GAAG,GAAK,EAAK,WAAW,IAAI,GAAK,EAAK,SAAS,IAAI,EAAG,OAAO,EAClF,IAAM,EAAQ,gDAAgD,KAAK,CAAI,EACvE,GAAI,CAAC,IAAQ,GAAI,OAAO,EACxB,IAAM,EAAS,EAAM,EAAE,EAAE,WAAW,IAAK,IAAI,EAC7C,MAAO,GAAG,EAAM,EAAE,CAAC,YAAY,EAAE,KAAK,GAAU,IAClD"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/adapters/node/files.d.ts
|
|
2
|
+
declare function computeFileTag(bytes: Uint8Array): string;
|
|
3
|
+
declare function decodeUtf8(bytes: Uint8Array, path: string): string;
|
|
4
|
+
/** Return whether the current process can edit an existing file. */
|
|
5
|
+
declare function isWritableFile(path: string): Promise<boolean>;
|
|
6
|
+
declare function resolveInputPath(input: string, cwd: string): string;
|
|
7
|
+
/** Resolve the same existing-file variants as Pi's native read tool. */
|
|
8
|
+
declare function resolveReadInputPath(input: string, cwd: string): Promise<string>;
|
|
9
|
+
declare function displayPath(absolutePath: string, cwd: string): string;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { computeFileTag, decodeUtf8, displayPath, isWritableFile, resolveInputPath, resolveReadInputPath };
|
|
12
|
+
//# sourceMappingURL=files.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.d.cts","names":[],"sources":["../../../src/adapters/node/files.ts"],"mappings":";iBAUgB,eAAe,OAAO;iBAItB,WAAW,OAAO,YAAY;;iBASxB,eAAe,eAAe;iBAOpC,iBAAiB,eAAe;;iBAW1B,qBAAqB,eAAe,cAAc;iBAgBxD,YAAY,sBAAsB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/adapters/node/files.d.ts
|
|
2
|
+
declare function computeFileTag(bytes: Uint8Array): string;
|
|
3
|
+
declare function decodeUtf8(bytes: Uint8Array, path: string): string;
|
|
4
|
+
/** Return whether the current process can edit an existing file. */
|
|
5
|
+
declare function isWritableFile(path: string): Promise<boolean>;
|
|
6
|
+
declare function resolveInputPath(input: string, cwd: string): string;
|
|
7
|
+
/** Resolve the same existing-file variants as Pi's native read tool. */
|
|
8
|
+
declare function resolveReadInputPath(input: string, cwd: string): Promise<string>;
|
|
9
|
+
declare function displayPath(absolutePath: string, cwd: string): string;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { computeFileTag, decodeUtf8, displayPath, isWritableFile, resolveInputPath, resolveReadInputPath };
|
|
12
|
+
//# sourceMappingURL=files.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.d.mts","names":[],"sources":["../../../src/adapters/node/files.ts"],"mappings":";iBAUgB,eAAe,OAAO;iBAItB,WAAW,OAAO,YAAY;;iBASxB,eAAe,eAAe;iBAOpC,iBAAiB,eAAe;;iBAW1B,qBAAqB,eAAe,cAAc;iBAgBxD,YAAY,sBAAsB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createHash as e}from"node:crypto";import{constants as t}from"node:fs";import{access as n}from"node:fs/promises";import{homedir as r}from"node:os";import{isAbsolute as i,relative as a,resolve as o,sep as s}from"node:path";import{fileURLToPath as c}from"node:url";const l=/[\u00A0\u2000-\u200A\u202F\u205F\u3000]/gu;function u(t){return e(`sha256`).update(t).digest(`base64url`).slice(0,8)}function d(e,t){try{return new TextDecoder(`utf-8`,{fatal:!0}).decode(e)}catch(e){throw Error(`Cannot use hashline tools on ${t}: the file is not valid UTF-8.`,{cause:e})}}async function f(e){return n(e,t.W_OK).then(()=>!0,()=>!1)}function p(e,t){let n=e.replace(l,` `);return n.startsWith(`@`)&&(n=n.slice(1)),process.platform===`win32`&&(n=_(n)),n===`~`?n=r():n.startsWith(`~${s}`)?n=o(r(),n.slice(2)):n.startsWith(`file://`)&&(n=c(n)),i(n)?o(n):o(t,n)}async function m(e,t){let n=p(e,t);if(await g(n))return n;let r=[n.replace(/ (AM|PM)\./giu,` $1.`),n.normalize(`NFD`),n.replaceAll(`'`,`’`),n.normalize(`NFD`).replaceAll(`'`,`’`)];for(let e of r)if(e!==n&&await g(e))return e;return n}function h(e,t){let n=a(t,e),r=(n!==``&&n!==`..`&&!n.startsWith(`..${s}`)&&!i(n)?n:e).split(s).join(`/`);return r===`~`||r.startsWith(`~/`)?`./${r}`:r}async function g(e){return n(e).then(()=>!0,()=>!1)}function _(e){if(!e.startsWith(`/`)||e.startsWith(`//`)||e.includes(`\\`))return e;let t=/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/iu.exec(e);if(!t?.[1])return e;let n=t[2]?.replaceAll(`/`,`\\`);return`${t[1].toUpperCase()}:\\${n??``}`}export{u as computeFileTag,d as decodeUtf8,h as displayPath,f as isWritableFile,p as resolveInputPath,m as resolveReadInputPath};
|
|
2
|
+
//# sourceMappingURL=files.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.mjs","names":[],"sources":["../../../src/adapters/node/files.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { constants } from 'node:fs';\nimport { access } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { isAbsolute, relative, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/gu;\nconst NARROW_NO_BREAK_SPACE = '\\u202f';\n\nexport function computeFileTag(bytes: Uint8Array): string {\n return createHash('sha256').update(bytes).digest('base64url').slice(0, 8);\n}\n\nexport function decodeUtf8(bytes: Uint8Array, path: string): string {\n try {\n return new TextDecoder('utf-8', { fatal: true }).decode(bytes);\n } catch (error) {\n throw new Error(`Cannot use hashline tools on ${path}: the file is not valid UTF-8.`, { cause: error });\n }\n}\n\n/** Return whether the current process can edit an existing file. */\nexport async function isWritableFile(path: string): Promise<boolean> {\n return access(path, constants.W_OK).then(\n () => true,\n () => false,\n );\n}\n\nexport function resolveInputPath(input: string, cwd: string): string {\n let path = input.replace(UNICODE_SPACES, ' ');\n if (path.startsWith('@')) path = path.slice(1);\n if (process.platform === 'win32') path = normalizeWindowsShellPath(path);\n if (path === '~') path = homedir();\n else if (path.startsWith(`~${sep}`)) path = resolve(homedir(), path.slice(2));\n else if (path.startsWith('file://')) path = fileURLToPath(path);\n return isAbsolute(path) ? resolve(path) : resolve(cwd, path);\n}\n\n/** Resolve the same existing-file variants as Pi's native read tool. */\nexport async function resolveReadInputPath(input: string, cwd: string): Promise<string> {\n const resolved = resolveInputPath(input, cwd);\n if (await pathExists(resolved)) return resolved;\n\n const variants = [\n resolved.replace(/ (AM|PM)\\./giu, `${NARROW_NO_BREAK_SPACE}$1.`),\n resolved.normalize('NFD'),\n resolved.replaceAll(\"'\", '\\u2019'),\n resolved.normalize('NFD').replaceAll(\"'\", '\\u2019'),\n ];\n for (const candidate of variants) {\n if (candidate !== resolved && (await pathExists(candidate))) return candidate;\n }\n return resolved;\n}\n\nexport function displayPath(absolutePath: string, cwd: string): string {\n const path = relative(cwd, absolutePath);\n const display =\n path !== '' && path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path) ? path : absolutePath;\n const portable = display.split(sep).join('/');\n return portable === '~' || portable.startsWith('~/') ? `./${portable}` : portable;\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n return access(path).then(\n () => true,\n () => false,\n );\n}\n\nfunction normalizeWindowsShellPath(path: string): string {\n if (!path.startsWith('/') || path.startsWith('//') || path.includes('\\\\')) return path;\n const match = /^\\/(?:mnt\\/|cygdrive\\/)?([a-z])(?:\\/(.*))?$/iu.exec(path);\n if (!match?.[1]) return path;\n const suffix = match[2]?.replaceAll('/', '\\\\');\n return `${match[1].toUpperCase()}:\\\\${suffix ?? ''}`;\n}\n"],"mappings":"6QAOA,MAAM,EAAiB,4CAGvB,SAAgB,EAAe,EAA2B,CACxD,OAAO,EAAW,QAAQ,CAAC,CAAC,OAAO,CAAK,CAAC,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,EAAG,CAAC,CAC1E,CAEA,SAAgB,EAAW,EAAmB,EAAsB,CAClE,GAAI,CACF,OAAO,IAAI,YAAY,QAAS,CAAE,MAAO,EAAK,CAAC,CAAC,CAAC,OAAO,CAAK,CAC/D,OAAS,EAAO,CACd,MAAU,MAAM,gCAAgC,EAAK,gCAAiC,CAAE,MAAO,CAAM,CAAC,CACxG,CACF,CAGA,eAAsB,EAAe,EAAgC,CACnE,OAAO,EAAO,EAAM,EAAU,IAAI,CAAC,CAAC,SAC5B,OACA,EACR,CACF,CAEA,SAAgB,EAAiB,EAAe,EAAqB,CACnE,IAAI,EAAO,EAAM,QAAQ,EAAgB,GAAG,EAM5C,OALI,EAAK,WAAW,GAAG,IAAG,EAAO,EAAK,MAAM,CAAC,GACzC,QAAQ,WAAa,UAAS,EAAO,EAA0B,CAAI,GACnE,IAAS,IAAK,EAAO,EAAQ,EACxB,EAAK,WAAW,IAAI,GAAK,EAAG,EAAO,EAAQ,EAAQ,EAAG,EAAK,MAAM,CAAC,CAAC,EACnE,EAAK,WAAW,SAAS,IAAG,EAAO,EAAc,CAAI,GACvD,EAAW,CAAI,EAAI,EAAQ,CAAI,EAAI,EAAQ,EAAK,CAAI,CAC7D,CAGA,eAAsB,EAAqB,EAAe,EAA8B,CACtF,IAAM,EAAW,EAAiB,EAAO,CAAG,EAC5C,GAAI,MAAM,EAAW,CAAQ,EAAG,OAAO,EAEvC,IAAM,EAAW,CACf,EAAS,QAAQ,gBAAiB,MAA6B,EAC/D,EAAS,UAAU,KAAK,EACxB,EAAS,WAAW,IAAK,GAAQ,EACjC,EAAS,UAAU,KAAK,CAAC,CAAC,WAAW,IAAK,GAAQ,CACpD,EACA,IAAK,IAAM,KAAa,EACtB,GAAI,IAAc,GAAa,MAAM,EAAW,CAAS,EAAI,OAAO,EAEtE,OAAO,CACT,CAEA,SAAgB,EAAY,EAAsB,EAAqB,CACrE,IAAM,EAAO,EAAS,EAAK,CAAY,EAGjC,GADJ,IAAS,IAAM,IAAS,MAAQ,CAAC,EAAK,WAAW,KAAK,GAAK,GAAK,CAAC,EAAW,CAAI,EAAI,EAAO,EAAA,CACpE,MAAM,CAAG,CAAC,CAAC,KAAK,GAAG,EAC5C,OAAO,IAAa,KAAO,EAAS,WAAW,IAAI,EAAI,KAAK,IAAa,CAC3E,CAEA,eAAe,EAAW,EAAgC,CACxD,OAAO,EAAO,CAAI,CAAC,CAAC,SACZ,OACA,EACR,CACF,CAEA,SAAS,EAA0B,EAAsB,CACvD,GAAI,CAAC,EAAK,WAAW,GAAG,GAAK,EAAK,WAAW,IAAI,GAAK,EAAK,SAAS,IAAI,EAAG,OAAO,EAClF,IAAM,EAAQ,gDAAgD,KAAK,CAAI,EACvE,GAAI,CAAC,IAAQ,GAAI,OAAO,EACxB,IAAM,EAAS,EAAM,EAAE,EAAE,WAAW,IAAK,IAAI,EAC7C,MAAO,GAAG,EAAM,EAAE,CAAC,YAAY,EAAE,KAAK,GAAU,IAClD"}
|
package/dist/files.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./adapters/node/files.cjs");exports.computeFileTag=e.computeFileTag,exports.decodeUtf8=e.decodeUtf8,exports.displayPath=e.displayPath,exports.isWritableFile=e.isWritableFile,exports.resolveInputPath=e.resolveInputPath,exports.resolveReadInputPath=e.resolveReadInputPath;
|
package/dist/files.d.cts
ADDED
package/dist/files.d.mts
ADDED
package/dist/files.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{computeFileTag as e,decodeUtf8 as t,displayPath as n,isWritableFile as r,resolveInputPath as i,resolveReadInputPath as a}from"./adapters/node/files.mjs";export{e as computeFileTag,t as decodeUtf8,n as displayPath,r as isWritableFile,i as resolveInputPath,a as resolveReadInputPath};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/hashline.cjs");exports.applyHashlineEdits=e.applyHashlineEdits,exports.formatFileHeader=e.formatFileHeader,exports.formatTaggedLine=e.formatTaggedLine,exports.formatTaggedLines=e.formatTaggedLines,exports.hashLine=e.hashLine,exports.normalizeFileTag=e.normalizeFileTag,exports.normalizeToLf=e.normalizeToLf,exports.parseFileHeader=e.parseFileHeader,exports.parseLineAnchor=e.parseLineAnchor,exports.parseTaggedLine=e.parseTaggedLine,exports.splitLines=e.splitLines,exports.stripBom=e.stripBom;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { AppliedHashlineEdits, FileHeader, HashlineRangeInput, LineAnchor, ParsedTaggedLine, PreparedHashlineEdit, TaggedLineMarker, TaggedLinePrefix } from "./types/hashline.cjs";
|
|
2
|
+
import { applyHashlineEdits, formatFileHeader, formatTaggedLine, formatTaggedLines, hashLine, normalizeFileTag, normalizeToLf, parseFileHeader, parseLineAnchor, parseTaggedLine, splitLines, stripBom } from "./services/hashline.cjs";
|
|
3
|
+
export { type AppliedHashlineEdits, type FileHeader, type HashlineRangeInput, type LineAnchor, type ParsedTaggedLine, type PreparedHashlineEdit, type TaggedLineMarker, type TaggedLinePrefix, applyHashlineEdits, formatFileHeader, formatTaggedLine, formatTaggedLines, hashLine, normalizeFileTag, normalizeToLf, parseFileHeader, parseLineAnchor, parseTaggedLine, splitLines, stripBom };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { AppliedHashlineEdits, FileHeader, HashlineRangeInput, LineAnchor, ParsedTaggedLine, PreparedHashlineEdit, TaggedLineMarker, TaggedLinePrefix } from "./types/hashline.mjs";
|
|
2
|
+
import { applyHashlineEdits, formatFileHeader, formatTaggedLine, formatTaggedLines, hashLine, normalizeFileTag, normalizeToLf, parseFileHeader, parseLineAnchor, parseTaggedLine, splitLines, stripBom } from "./services/hashline.mjs";
|
|
3
|
+
export { type AppliedHashlineEdits, type FileHeader, type HashlineRangeInput, type LineAnchor, type ParsedTaggedLine, type PreparedHashlineEdit, type TaggedLineMarker, type TaggedLinePrefix, applyHashlineEdits, formatFileHeader, formatTaggedLine, formatTaggedLines, hashLine, normalizeFileTag, normalizeToLf, parseFileHeader, parseLineAnchor, parseTaggedLine, splitLines, stripBom };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{applyHashlineEdits as e,formatFileHeader as t,formatTaggedLine as n,formatTaggedLines as r,hashLine as i,normalizeFileTag as a,normalizeToLf as o,parseFileHeader as s,parseLineAnchor as c,parseTaggedLine as l,splitLines as u,stripBom as d}from"./services/hashline.mjs";export{e as applyHashlineEdits,t as formatFileHeader,n as formatTaggedLine,r as formatTaggedLines,i as hashLine,a as normalizeFileTag,o as normalizeToLf,s as parseFileHeader,c as parseLineAnchor,l as parseTaggedLine,u as splitLines,d as stripBom};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const e=26n*26n*26n,t=/^[A-Za-z0-9_-]{8}$/u,n=/^@file (.+)#([A-Za-z0-9_-]{8})$/u,r=/^(>> | {3})?(\d+)#[a-z]{3}\|(.*)$/u,i=/^\s*[>+-]*\s*(\d+)\s*[:#]\s*([a-zA-Z]{3})(?:\s*\|.*)?\s*$/u,a=/[ \t\n\r\v\f]/gu;function o(t){let n=new TextEncoder().encode(t.replace(a,``)),r=14695981039346656037n;for(let e of n)r^=BigInt(e),r=BigInt.asUintN(64,r*1099511628211n);let i=r%e,o=Array.from({length:3},()=>``);for(let e=o.length-1;e>=0;--e)o[e]=String.fromCharCode(97+Number(i%26n)),i/=26n;return o.join(``)}function s(e){return e.replace(/\r\n/gu,`
|
|
2
|
+
`).replace(/\r/gu,`
|
|
3
|
+
`)}function c(e){return e.charCodeAt(0)===65279?e.slice(1):e}function l(e){return s(c(e)).split(`
|
|
4
|
+
`)}function u(e){let n=e.trim(),r=n.lastIndexOf(`#`),i=r===-1?n:n.slice(r+1);if(!t.test(i))throw Error(`Invalid file hash: ${e}. Expected the eight-character tag from @file path#HASH.`);return i}function d(e,t){if(e.length===0||e.includes(`
|
|
5
|
+
`)||e.includes(`\r`))throw Error(`Invalid file header path. Expected one non-empty path without line breaks.`);return`@file ${e}#${u(t)}`}function f(e){let t=n.exec(e),r=t?.[1],i=t?.[2];return r===void 0||i===void 0?void 0:{path:r,tag:i}}function p(e,t,n=``,r=e){return y(t),`${n}${t}#${o(r)}|${e}`}function m(e,t=1){return e.map((e,n)=>p(e,t+n)).join(`
|
|
6
|
+
`)}function h(e){let t=r.exec(e),n=t?.[2],i=t?.[3];if(n===void 0||i===void 0)return;let a=Number.parseInt(n,10);if(!Number.isSafeInteger(a)||a<1)return;let o=v(t?.[1]);return o===void 0?{content:i,line:a}:{content:i,line:a,marker:o}}function g(e){if(e.includes(`
|
|
7
|
+
`)||e.includes(`\r`))throw Error(`Invalid line anchor: ${e}. Expected one anchor such as 5#abc, not a pasted multiline block.`);let t=i.exec(e);if(!t)throw Error(`Invalid line anchor: ${e}. Expected one anchor such as 5#abc from the latest read or grep result.`);let n=Number.parseInt(t[1]??``,10),r=t[2]?.toLowerCase();if(!Number.isSafeInteger(n)||n<1||r===void 0)throw Error(`Invalid line anchor: ${e}. Expected a positive line number and three letters, such as 5#abc.`);return{line:n,hash:r}}function _(e,t){let n=l(e),r=S(t.map(e=>b(e,n))).sort(C);w(r);let i=[...n];for(let e of[...r].reverse()){let t=e.to.line-e.from.line+1,n=e.content===null||e.content===``?[]:T(e.content);i.splice(e.from.line-1,t,...n)}return{content:i.join(`
|
|
8
|
+
`),edits:r}}function v(e){if(e===`>> `)return`match`;if(e===` `)return`context`}function y(e){if(!Number.isSafeInteger(e)||e<1)throw Error(`Invalid tagged line number: ${String(e)}. Expected a positive safe integer.`)}function b(e,t){let n=g(e.from),r=g(e.to);if(n.line>r.line)throw Error(`Invalid edit range ${e.from} to ${e.to}: the starting line follows the ending line.`);if(r.line>t.length)throw Error(`Invalid edit range ${e.from} to ${e.to}: the file has ${t.length} lines.`);return x(n,t),x(r,t),{from:n,to:r,content:e.content??null,source:e}}function x(e,t){let n=t[e.line-1];if(n===void 0)throw Error(`Line ${e.line} is outside the current file.`);let r=o(n);if(r!==e.hash)throw Error(`Stale line anchor ${e.line}#${e.hash}. Current anchor is ${e.line}#${r}. Re-read the file and retry.`)}function S(e){let t=new Map;for(let n of e){let e=JSON.stringify([n.from.line,n.from.hash,n.to.line,n.to.hash,n.content]);t.has(e)||t.set(e,n)}return[...t.values()]}function C(e,t){return e.from.line-t.from.line||e.to.line-t.to.line}function w(e){for(let t=1;t<e.length;t+=1){let n=e[t-1],r=e[t];if(n!==void 0&&r!==void 0&&r.from.line<=n.to.line)throw Error(`Overlapping edit ranges ${n.from.line}-${n.to.line} and ${r.from.line}-${r.to.line}. Merge them and retry.`)}}function T(e){let t=s(e),n=t.endsWith(`
|
|
9
|
+
`)?t.slice(0,-1):t;return n===``?[]:n.split(`
|
|
10
|
+
`)}exports.applyHashlineEdits=_,exports.formatFileHeader=d,exports.formatTaggedLine=p,exports.formatTaggedLines=m,exports.hashLine=o,exports.normalizeFileTag=u,exports.normalizeToLf=s,exports.parseFileHeader=f,exports.parseLineAnchor=g,exports.parseTaggedLine=h,exports.splitLines=l,exports.stripBom=c;
|
|
11
|
+
//# sourceMappingURL=hashline.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hashline.cjs","names":[],"sources":["../../src/services/hashline.ts"],"sourcesContent":["import type {\n AppliedHashlineEdits,\n FileHeader,\n HashlineRangeInput,\n LineAnchor,\n ParsedTaggedLine,\n PreparedHashlineEdit,\n TaggedLineMarker,\n TaggedLinePrefix,\n} from '../types/hashline.ts';\n\nconst FNV_OFFSET_BASIS = 0xcbf29ce484222325n;\nconst FNV_PRIME = 0x100000001b3n;\nconst ANCHOR_SPACE = 26n * 26n * 26n;\nconst ASCII_A = 97;\nconst FILE_TAG_PATTERN = /^[A-Za-z0-9_-]{8}$/u;\nconst FILE_HEADER_PATTERN = /^@file (.+)#([A-Za-z0-9_-]{8})$/u;\nconst TAGGED_LINE_PATTERN = /^(>> | {3})?(\\d+)#[a-z]{3}\\|(.*)$/u;\nconst ANCHOR_PATTERN = /^\\s*[>+-]*\\s*(\\d+)\\s*[:#]\\s*([a-zA-Z]{3})(?:\\s*\\|.*)?\\s*$/u;\nconst ASCII_WHITESPACE = /[ \\t\\n\\r\\v\\f]/gu;\n\n/**\n * Return Phi's three-letter whitespace-insensitive FNV-1a64 line anchor.\n * Adapted from pulseaiclub/phi writetool under the MIT license.\n */\nexport function hashLine(line: string): string {\n const bytes = new TextEncoder().encode(line.replace(ASCII_WHITESPACE, ''));\n let hash = FNV_OFFSET_BASIS;\n for (const byte of bytes) {\n hash ^= BigInt(byte);\n hash = BigInt.asUintN(64, hash * FNV_PRIME);\n }\n\n let value = hash % ANCHOR_SPACE;\n const letters = Array.from({ length: 3 }, () => '');\n for (let index = letters.length - 1; index >= 0; index -= 1) {\n letters[index] = String.fromCharCode(ASCII_A + Number(value % 26n));\n value /= 26n;\n }\n return letters.join('');\n}\n\nexport function normalizeToLf(text: string): string {\n return text.replace(/\\r\\n/gu, '\\n').replace(/\\r/gu, '\\n');\n}\n\nexport function stripBom(text: string): string {\n return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n}\n\nexport function splitLines(text: string): string[] {\n return normalizeToLf(stripBom(text)).split('\\n');\n}\n\nexport function normalizeFileTag(value: string): string {\n const trimmed = value.trim();\n const hashIndex = trimmed.lastIndexOf('#');\n const tag = hashIndex === -1 ? trimmed : trimmed.slice(hashIndex + 1);\n if (!FILE_TAG_PATTERN.test(tag)) {\n throw new Error(`Invalid file hash: ${value}. Expected the eight-character tag from @file path#HASH.`);\n }\n return tag;\n}\n\nexport function formatFileHeader(path: string, tag: string): string {\n if (path.length === 0 || path.includes('\\n') || path.includes('\\r')) {\n throw new Error('Invalid file header path. Expected one non-empty path without line breaks.');\n }\n return `@file ${path}#${normalizeFileTag(tag)}`;\n}\n\nexport function parseFileHeader(value: string): FileHeader | undefined {\n const match = FILE_HEADER_PATTERN.exec(value);\n const path = match?.[1];\n const tag = match?.[2];\n return path === undefined || tag === undefined ? undefined : { path, tag };\n}\n\nexport function formatTaggedLine(\n line: string,\n lineNumber: number,\n prefix: TaggedLinePrefix = '',\n anchorSource = line,\n): string {\n assertLineNumber(lineNumber);\n return `${prefix}${lineNumber}#${hashLine(anchorSource)}|${line}`;\n}\n\nexport function formatTaggedLines(lines: readonly string[], firstLine = 1): string {\n return lines.map((line, index) => formatTaggedLine(line, firstLine + index)).join('\\n');\n}\n\nexport function parseTaggedLine(value: string): ParsedTaggedLine | undefined {\n const match = TAGGED_LINE_PATTERN.exec(value);\n const lineText = match?.[2];\n const content = match?.[3];\n if (lineText === undefined || content === undefined) return undefined;\n\n const line = Number.parseInt(lineText, 10);\n if (!Number.isSafeInteger(line) || line < 1) return undefined;\n\n const marker = taggedLineMarker(match?.[1]);\n return marker === undefined ? { content, line } : { content, line, marker };\n}\n\nexport function parseLineAnchor(value: string): LineAnchor {\n if (value.includes('\\n') || value.includes('\\r')) {\n throw new Error(`Invalid line anchor: ${value}. Expected one anchor such as 5#abc, not a pasted multiline block.`);\n }\n\n const match = ANCHOR_PATTERN.exec(value);\n if (!match) {\n throw new Error(\n `Invalid line anchor: ${value}. Expected one anchor such as 5#abc from the latest read or grep result.`,\n );\n }\n\n const line = Number.parseInt(match[1] ?? '', 10);\n const hash = match[2]?.toLowerCase();\n if (!Number.isSafeInteger(line) || line < 1 || hash === undefined) {\n throw new Error(`Invalid line anchor: ${value}. Expected a positive line number and three letters, such as 5#abc.`);\n }\n return { line, hash };\n}\n\nexport function applyHashlineEdits(\n originalContent: string,\n requestedEdits: readonly HashlineRangeInput[],\n): AppliedHashlineEdits {\n const lines = splitLines(originalContent);\n const validated = requestedEdits.map((edit) => validateEdit(edit, lines));\n const edits = deduplicateEdits(validated).sort(compareEdits);\n validateNoOverlaps(edits);\n\n const result = [...lines];\n for (const edit of [...edits].reverse()) {\n const deleteCount = edit.to.line - edit.from.line + 1;\n const replacements = edit.content === null || edit.content === '' ? [] : splitReplacement(edit.content);\n result.splice(edit.from.line - 1, deleteCount, ...replacements);\n }\n return { content: result.join('\\n'), edits };\n}\n\nfunction taggedLineMarker(prefix: string | undefined): TaggedLineMarker | undefined {\n if (prefix === '>> ') return 'match';\n if (prefix === ' ') return 'context';\n return undefined;\n}\n\nfunction assertLineNumber(line: number): void {\n if (!Number.isSafeInteger(line) || line < 1) {\n throw new Error(`Invalid tagged line number: ${String(line)}. Expected a positive safe integer.`);\n }\n}\n\nfunction validateEdit(edit: HashlineRangeInput, lines: readonly string[]): PreparedHashlineEdit {\n const from = parseLineAnchor(edit.from);\n const to = parseLineAnchor(edit.to);\n if (from.line > to.line) {\n throw new Error(`Invalid edit range ${edit.from} to ${edit.to}: the starting line follows the ending line.`);\n }\n if (to.line > lines.length) {\n throw new Error(`Invalid edit range ${edit.from} to ${edit.to}: the file has ${lines.length} lines.`);\n }\n\n validateAnchor(from, lines);\n validateAnchor(to, lines);\n return { from, to, content: edit.content ?? null, source: edit };\n}\n\nfunction validateAnchor(anchor: LineAnchor, lines: readonly string[]): void {\n const line = lines[anchor.line - 1];\n if (line === undefined) throw new Error(`Line ${anchor.line} is outside the current file.`);\n const currentHash = hashLine(line);\n if (currentHash !== anchor.hash) {\n throw new Error(\n `Stale line anchor ${anchor.line}#${anchor.hash}. Current anchor is ${anchor.line}#${currentHash}. Re-read the file and retry.`,\n );\n }\n}\n\nfunction deduplicateEdits(edits: readonly PreparedHashlineEdit[]): PreparedHashlineEdit[] {\n const unique = new Map<string, PreparedHashlineEdit>();\n for (const edit of edits) {\n const key = JSON.stringify([edit.from.line, edit.from.hash, edit.to.line, edit.to.hash, edit.content]);\n if (!unique.has(key)) unique.set(key, edit);\n }\n return [...unique.values()];\n}\n\nfunction compareEdits(left: PreparedHashlineEdit, right: PreparedHashlineEdit): number {\n return left.from.line - right.from.line || left.to.line - right.to.line;\n}\n\nfunction validateNoOverlaps(edits: readonly PreparedHashlineEdit[]): void {\n for (let index = 1; index < edits.length; index += 1) {\n const previous = edits[index - 1];\n const current = edits[index];\n if (previous !== undefined && current !== undefined && current.from.line <= previous.to.line) {\n throw new Error(\n `Overlapping edit ranges ${previous.from.line}-${previous.to.line} and ${current.from.line}-${current.to.line}. Merge them and retry.`,\n );\n }\n }\n}\n\nfunction splitReplacement(content: string): string[] {\n const normalized = normalizeToLf(content);\n const trimmed = normalized.endsWith('\\n') ? normalized.slice(0, -1) : normalized;\n return trimmed === '' ? [] : trimmed.split('\\n');\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AppliedHashlineEdits, FileHeader, HashlineRangeInput, LineAnchor, ParsedTaggedLine, TaggedLinePrefix } from "../types/hashline.cjs";
|
|
2
|
+
//#region src/services/hashline.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Return Phi's three-letter whitespace-insensitive FNV-1a64 line anchor.
|
|
5
|
+
* Adapted from pulseaiclub/phi writetool under the MIT license.
|
|
6
|
+
*/
|
|
7
|
+
declare function hashLine(line: string): string;
|
|
8
|
+
declare function normalizeToLf(text: string): string;
|
|
9
|
+
declare function stripBom(text: string): string;
|
|
10
|
+
declare function splitLines(text: string): string[];
|
|
11
|
+
declare function normalizeFileTag(value: string): string;
|
|
12
|
+
declare function formatFileHeader(path: string, tag: string): string;
|
|
13
|
+
declare function parseFileHeader(value: string): FileHeader | undefined;
|
|
14
|
+
declare function formatTaggedLine(line: string, lineNumber: number, prefix?: TaggedLinePrefix, anchorSource?: string): string;
|
|
15
|
+
declare function formatTaggedLines(lines: readonly string[], firstLine?: number): string;
|
|
16
|
+
declare function parseTaggedLine(value: string): ParsedTaggedLine | undefined;
|
|
17
|
+
declare function parseLineAnchor(value: string): LineAnchor;
|
|
18
|
+
declare function applyHashlineEdits(originalContent: string, requestedEdits: readonly HashlineRangeInput[]): AppliedHashlineEdits;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { applyHashlineEdits, formatFileHeader, formatTaggedLine, formatTaggedLines, hashLine, normalizeFileTag, normalizeToLf, parseFileHeader, parseLineAnchor, parseTaggedLine, splitLines, stripBom };
|
|
21
|
+
//# sourceMappingURL=hashline.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hashline.d.cts","names":[],"sources":["../../src/services/hashline.ts"],"mappings":";;;;;;iBAyBgB,SAAS;iBAiBT,cAAc;iBAId,SAAS;iBAIT,WAAW;iBAIX,iBAAiB;iBAUjB,iBAAiB,cAAc;iBAO/B,gBAAgB,gBAAgB;iBAOhC,iBACd,cACA,oBACA,SAAQ,kBACR;iBAMc,kBAAkB,0BAA0B;iBAI5C,gBAAgB,gBAAgB;iBAahC,gBAAgB,gBAAgB;iBAoBhC,mBACd,yBACA,yBAAyB,uBACxB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AppliedHashlineEdits, FileHeader, HashlineRangeInput, LineAnchor, ParsedTaggedLine, TaggedLinePrefix } from "../types/hashline.mjs";
|
|
2
|
+
//#region src/services/hashline.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Return Phi's three-letter whitespace-insensitive FNV-1a64 line anchor.
|
|
5
|
+
* Adapted from pulseaiclub/phi writetool under the MIT license.
|
|
6
|
+
*/
|
|
7
|
+
declare function hashLine(line: string): string;
|
|
8
|
+
declare function normalizeToLf(text: string): string;
|
|
9
|
+
declare function stripBom(text: string): string;
|
|
10
|
+
declare function splitLines(text: string): string[];
|
|
11
|
+
declare function normalizeFileTag(value: string): string;
|
|
12
|
+
declare function formatFileHeader(path: string, tag: string): string;
|
|
13
|
+
declare function parseFileHeader(value: string): FileHeader | undefined;
|
|
14
|
+
declare function formatTaggedLine(line: string, lineNumber: number, prefix?: TaggedLinePrefix, anchorSource?: string): string;
|
|
15
|
+
declare function formatTaggedLines(lines: readonly string[], firstLine?: number): string;
|
|
16
|
+
declare function parseTaggedLine(value: string): ParsedTaggedLine | undefined;
|
|
17
|
+
declare function parseLineAnchor(value: string): LineAnchor;
|
|
18
|
+
declare function applyHashlineEdits(originalContent: string, requestedEdits: readonly HashlineRangeInput[]): AppliedHashlineEdits;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { applyHashlineEdits, formatFileHeader, formatTaggedLine, formatTaggedLines, hashLine, normalizeFileTag, normalizeToLf, parseFileHeader, parseLineAnchor, parseTaggedLine, splitLines, stripBom };
|
|
21
|
+
//# sourceMappingURL=hashline.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hashline.d.mts","names":[],"sources":["../../src/services/hashline.ts"],"mappings":";;;;;;iBAyBgB,SAAS;iBAiBT,cAAc;iBAId,SAAS;iBAIT,WAAW;iBAIX,iBAAiB;iBAUjB,iBAAiB,cAAc;iBAO/B,gBAAgB,gBAAgB;iBAOhC,iBACd,cACA,oBACA,SAAQ,kBACR;iBAMc,kBAAkB,0BAA0B;iBAI5C,gBAAgB,gBAAgB;iBAahC,gBAAgB,gBAAgB;iBAoBhC,mBACd,yBACA,yBAAyB,uBACxB"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const e=26n*26n*26n,t=/^[A-Za-z0-9_-]{8}$/u,n=/^@file (.+)#([A-Za-z0-9_-]{8})$/u,r=/^(>> | {3})?(\d+)#[a-z]{3}\|(.*)$/u,i=/^\s*[>+-]*\s*(\d+)\s*[:#]\s*([a-zA-Z]{3})(?:\s*\|.*)?\s*$/u,a=/[ \t\n\r\v\f]/gu;function o(t){let n=new TextEncoder().encode(t.replace(a,``)),r=14695981039346656037n;for(let e of n)r^=BigInt(e),r=BigInt.asUintN(64,r*1099511628211n);let i=r%e,o=Array.from({length:3},()=>``);for(let e=o.length-1;e>=0;--e)o[e]=String.fromCharCode(97+Number(i%26n)),i/=26n;return o.join(``)}function s(e){return e.replace(/\r\n/gu,`
|
|
2
|
+
`).replace(/\r/gu,`
|
|
3
|
+
`)}function c(e){return e.charCodeAt(0)===65279?e.slice(1):e}function l(e){return s(c(e)).split(`
|
|
4
|
+
`)}function u(e){let n=e.trim(),r=n.lastIndexOf(`#`),i=r===-1?n:n.slice(r+1);if(!t.test(i))throw Error(`Invalid file hash: ${e}. Expected the eight-character tag from @file path#HASH.`);return i}function d(e,t){if(e.length===0||e.includes(`
|
|
5
|
+
`)||e.includes(`\r`))throw Error(`Invalid file header path. Expected one non-empty path without line breaks.`);return`@file ${e}#${u(t)}`}function f(e){let t=n.exec(e),r=t?.[1],i=t?.[2];return r===void 0||i===void 0?void 0:{path:r,tag:i}}function p(e,t,n=``,r=e){return y(t),`${n}${t}#${o(r)}|${e}`}function m(e,t=1){return e.map((e,n)=>p(e,t+n)).join(`
|
|
6
|
+
`)}function h(e){let t=r.exec(e),n=t?.[2],i=t?.[3];if(n===void 0||i===void 0)return;let a=Number.parseInt(n,10);if(!Number.isSafeInteger(a)||a<1)return;let o=v(t?.[1]);return o===void 0?{content:i,line:a}:{content:i,line:a,marker:o}}function g(e){if(e.includes(`
|
|
7
|
+
`)||e.includes(`\r`))throw Error(`Invalid line anchor: ${e}. Expected one anchor such as 5#abc, not a pasted multiline block.`);let t=i.exec(e);if(!t)throw Error(`Invalid line anchor: ${e}. Expected one anchor such as 5#abc from the latest read or grep result.`);let n=Number.parseInt(t[1]??``,10),r=t[2]?.toLowerCase();if(!Number.isSafeInteger(n)||n<1||r===void 0)throw Error(`Invalid line anchor: ${e}. Expected a positive line number and three letters, such as 5#abc.`);return{line:n,hash:r}}function _(e,t){let n=l(e),r=S(t.map(e=>b(e,n))).sort(C);w(r);let i=[...n];for(let e of[...r].reverse()){let t=e.to.line-e.from.line+1,n=e.content===null||e.content===``?[]:T(e.content);i.splice(e.from.line-1,t,...n)}return{content:i.join(`
|
|
8
|
+
`),edits:r}}function v(e){if(e===`>> `)return`match`;if(e===` `)return`context`}function y(e){if(!Number.isSafeInteger(e)||e<1)throw Error(`Invalid tagged line number: ${String(e)}. Expected a positive safe integer.`)}function b(e,t){let n=g(e.from),r=g(e.to);if(n.line>r.line)throw Error(`Invalid edit range ${e.from} to ${e.to}: the starting line follows the ending line.`);if(r.line>t.length)throw Error(`Invalid edit range ${e.from} to ${e.to}: the file has ${t.length} lines.`);return x(n,t),x(r,t),{from:n,to:r,content:e.content??null,source:e}}function x(e,t){let n=t[e.line-1];if(n===void 0)throw Error(`Line ${e.line} is outside the current file.`);let r=o(n);if(r!==e.hash)throw Error(`Stale line anchor ${e.line}#${e.hash}. Current anchor is ${e.line}#${r}. Re-read the file and retry.`)}function S(e){let t=new Map;for(let n of e){let e=JSON.stringify([n.from.line,n.from.hash,n.to.line,n.to.hash,n.content]);t.has(e)||t.set(e,n)}return[...t.values()]}function C(e,t){return e.from.line-t.from.line||e.to.line-t.to.line}function w(e){for(let t=1;t<e.length;t+=1){let n=e[t-1],r=e[t];if(n!==void 0&&r!==void 0&&r.from.line<=n.to.line)throw Error(`Overlapping edit ranges ${n.from.line}-${n.to.line} and ${r.from.line}-${r.to.line}. Merge them and retry.`)}}function T(e){let t=s(e),n=t.endsWith(`
|
|
9
|
+
`)?t.slice(0,-1):t;return n===``?[]:n.split(`
|
|
10
|
+
`)}export{_ as applyHashlineEdits,d as formatFileHeader,p as formatTaggedLine,m as formatTaggedLines,o as hashLine,u as normalizeFileTag,s as normalizeToLf,f as parseFileHeader,g as parseLineAnchor,h as parseTaggedLine,l as splitLines,c as stripBom};
|
|
11
|
+
//# sourceMappingURL=hashline.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hashline.mjs","names":[],"sources":["../../src/services/hashline.ts"],"sourcesContent":["import type {\n AppliedHashlineEdits,\n FileHeader,\n HashlineRangeInput,\n LineAnchor,\n ParsedTaggedLine,\n PreparedHashlineEdit,\n TaggedLineMarker,\n TaggedLinePrefix,\n} from '../types/hashline.ts';\n\nconst FNV_OFFSET_BASIS = 0xcbf29ce484222325n;\nconst FNV_PRIME = 0x100000001b3n;\nconst ANCHOR_SPACE = 26n * 26n * 26n;\nconst ASCII_A = 97;\nconst FILE_TAG_PATTERN = /^[A-Za-z0-9_-]{8}$/u;\nconst FILE_HEADER_PATTERN = /^@file (.+)#([A-Za-z0-9_-]{8})$/u;\nconst TAGGED_LINE_PATTERN = /^(>> | {3})?(\\d+)#[a-z]{3}\\|(.*)$/u;\nconst ANCHOR_PATTERN = /^\\s*[>+-]*\\s*(\\d+)\\s*[:#]\\s*([a-zA-Z]{3})(?:\\s*\\|.*)?\\s*$/u;\nconst ASCII_WHITESPACE = /[ \\t\\n\\r\\v\\f]/gu;\n\n/**\n * Return Phi's three-letter whitespace-insensitive FNV-1a64 line anchor.\n * Adapted from pulseaiclub/phi writetool under the MIT license.\n */\nexport function hashLine(line: string): string {\n const bytes = new TextEncoder().encode(line.replace(ASCII_WHITESPACE, ''));\n let hash = FNV_OFFSET_BASIS;\n for (const byte of bytes) {\n hash ^= BigInt(byte);\n hash = BigInt.asUintN(64, hash * FNV_PRIME);\n }\n\n let value = hash % ANCHOR_SPACE;\n const letters = Array.from({ length: 3 }, () => '');\n for (let index = letters.length - 1; index >= 0; index -= 1) {\n letters[index] = String.fromCharCode(ASCII_A + Number(value % 26n));\n value /= 26n;\n }\n return letters.join('');\n}\n\nexport function normalizeToLf(text: string): string {\n return text.replace(/\\r\\n/gu, '\\n').replace(/\\r/gu, '\\n');\n}\n\nexport function stripBom(text: string): string {\n return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n}\n\nexport function splitLines(text: string): string[] {\n return normalizeToLf(stripBom(text)).split('\\n');\n}\n\nexport function normalizeFileTag(value: string): string {\n const trimmed = value.trim();\n const hashIndex = trimmed.lastIndexOf('#');\n const tag = hashIndex === -1 ? trimmed : trimmed.slice(hashIndex + 1);\n if (!FILE_TAG_PATTERN.test(tag)) {\n throw new Error(`Invalid file hash: ${value}. Expected the eight-character tag from @file path#HASH.`);\n }\n return tag;\n}\n\nexport function formatFileHeader(path: string, tag: string): string {\n if (path.length === 0 || path.includes('\\n') || path.includes('\\r')) {\n throw new Error('Invalid file header path. Expected one non-empty path without line breaks.');\n }\n return `@file ${path}#${normalizeFileTag(tag)}`;\n}\n\nexport function parseFileHeader(value: string): FileHeader | undefined {\n const match = FILE_HEADER_PATTERN.exec(value);\n const path = match?.[1];\n const tag = match?.[2];\n return path === undefined || tag === undefined ? undefined : { path, tag };\n}\n\nexport function formatTaggedLine(\n line: string,\n lineNumber: number,\n prefix: TaggedLinePrefix = '',\n anchorSource = line,\n): string {\n assertLineNumber(lineNumber);\n return `${prefix}${lineNumber}#${hashLine(anchorSource)}|${line}`;\n}\n\nexport function formatTaggedLines(lines: readonly string[], firstLine = 1): string {\n return lines.map((line, index) => formatTaggedLine(line, firstLine + index)).join('\\n');\n}\n\nexport function parseTaggedLine(value: string): ParsedTaggedLine | undefined {\n const match = TAGGED_LINE_PATTERN.exec(value);\n const lineText = match?.[2];\n const content = match?.[3];\n if (lineText === undefined || content === undefined) return undefined;\n\n const line = Number.parseInt(lineText, 10);\n if (!Number.isSafeInteger(line) || line < 1) return undefined;\n\n const marker = taggedLineMarker(match?.[1]);\n return marker === undefined ? { content, line } : { content, line, marker };\n}\n\nexport function parseLineAnchor(value: string): LineAnchor {\n if (value.includes('\\n') || value.includes('\\r')) {\n throw new Error(`Invalid line anchor: ${value}. Expected one anchor such as 5#abc, not a pasted multiline block.`);\n }\n\n const match = ANCHOR_PATTERN.exec(value);\n if (!match) {\n throw new Error(\n `Invalid line anchor: ${value}. Expected one anchor such as 5#abc from the latest read or grep result.`,\n );\n }\n\n const line = Number.parseInt(match[1] ?? '', 10);\n const hash = match[2]?.toLowerCase();\n if (!Number.isSafeInteger(line) || line < 1 || hash === undefined) {\n throw new Error(`Invalid line anchor: ${value}. Expected a positive line number and three letters, such as 5#abc.`);\n }\n return { line, hash };\n}\n\nexport function applyHashlineEdits(\n originalContent: string,\n requestedEdits: readonly HashlineRangeInput[],\n): AppliedHashlineEdits {\n const lines = splitLines(originalContent);\n const validated = requestedEdits.map((edit) => validateEdit(edit, lines));\n const edits = deduplicateEdits(validated).sort(compareEdits);\n validateNoOverlaps(edits);\n\n const result = [...lines];\n for (const edit of [...edits].reverse()) {\n const deleteCount = edit.to.line - edit.from.line + 1;\n const replacements = edit.content === null || edit.content === '' ? [] : splitReplacement(edit.content);\n result.splice(edit.from.line - 1, deleteCount, ...replacements);\n }\n return { content: result.join('\\n'), edits };\n}\n\nfunction taggedLineMarker(prefix: string | undefined): TaggedLineMarker | undefined {\n if (prefix === '>> ') return 'match';\n if (prefix === ' ') return 'context';\n return undefined;\n}\n\nfunction assertLineNumber(line: number): void {\n if (!Number.isSafeInteger(line) || line < 1) {\n throw new Error(`Invalid tagged line number: ${String(line)}. Expected a positive safe integer.`);\n }\n}\n\nfunction validateEdit(edit: HashlineRangeInput, lines: readonly string[]): PreparedHashlineEdit {\n const from = parseLineAnchor(edit.from);\n const to = parseLineAnchor(edit.to);\n if (from.line > to.line) {\n throw new Error(`Invalid edit range ${edit.from} to ${edit.to}: the starting line follows the ending line.`);\n }\n if (to.line > lines.length) {\n throw new Error(`Invalid edit range ${edit.from} to ${edit.to}: the file has ${lines.length} lines.`);\n }\n\n validateAnchor(from, lines);\n validateAnchor(to, lines);\n return { from, to, content: edit.content ?? null, source: edit };\n}\n\nfunction validateAnchor(anchor: LineAnchor, lines: readonly string[]): void {\n const line = lines[anchor.line - 1];\n if (line === undefined) throw new Error(`Line ${anchor.line} is outside the current file.`);\n const currentHash = hashLine(line);\n if (currentHash !== anchor.hash) {\n throw new Error(\n `Stale line anchor ${anchor.line}#${anchor.hash}. Current anchor is ${anchor.line}#${currentHash}. Re-read the file and retry.`,\n );\n }\n}\n\nfunction deduplicateEdits(edits: readonly PreparedHashlineEdit[]): PreparedHashlineEdit[] {\n const unique = new Map<string, PreparedHashlineEdit>();\n for (const edit of edits) {\n const key = JSON.stringify([edit.from.line, edit.from.hash, edit.to.line, edit.to.hash, edit.content]);\n if (!unique.has(key)) unique.set(key, edit);\n }\n return [...unique.values()];\n}\n\nfunction compareEdits(left: PreparedHashlineEdit, right: PreparedHashlineEdit): number {\n return left.from.line - right.from.line || left.to.line - right.to.line;\n}\n\nfunction validateNoOverlaps(edits: readonly PreparedHashlineEdit[]): void {\n for (let index = 1; index < edits.length; index += 1) {\n const previous = edits[index - 1];\n const current = edits[index];\n if (previous !== undefined && current !== undefined && current.from.line <= previous.to.line) {\n throw new Error(\n `Overlapping edit ranges ${previous.from.line}-${previous.to.line} and ${current.from.line}-${current.to.line}. Merge them and retry.`,\n );\n }\n }\n}\n\nfunction splitReplacement(content: string): string[] {\n const normalized = normalizeToLf(content);\n const trimmed = normalized.endsWith('\\n') ? normalized.slice(0, -1) : normalized;\n return trimmed === '' ? [] : trimmed.split('\\n');\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/types/hashline.d.ts
|
|
2
|
+
interface HashlineRangeInput {
|
|
3
|
+
readonly from: string;
|
|
4
|
+
readonly to: string;
|
|
5
|
+
readonly content?: string | null;
|
|
6
|
+
}
|
|
7
|
+
interface LineAnchor {
|
|
8
|
+
readonly line: number;
|
|
9
|
+
readonly hash: string;
|
|
10
|
+
}
|
|
11
|
+
interface PreparedHashlineEdit {
|
|
12
|
+
readonly from: LineAnchor;
|
|
13
|
+
readonly to: LineAnchor;
|
|
14
|
+
readonly content: string | null;
|
|
15
|
+
readonly source: HashlineRangeInput;
|
|
16
|
+
}
|
|
17
|
+
interface AppliedHashlineEdits {
|
|
18
|
+
readonly content: string;
|
|
19
|
+
readonly edits: readonly PreparedHashlineEdit[];
|
|
20
|
+
}
|
|
21
|
+
interface FileHeader {
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly tag: string;
|
|
24
|
+
}
|
|
25
|
+
type TaggedLineMarker = 'match' | 'context';
|
|
26
|
+
interface ParsedTaggedLine {
|
|
27
|
+
readonly content: string;
|
|
28
|
+
readonly line: number;
|
|
29
|
+
readonly marker?: TaggedLineMarker;
|
|
30
|
+
}
|
|
31
|
+
type TaggedLinePrefix = '' | '>> ' | ' ';
|
|
32
|
+
//#endregion
|
|
33
|
+
export { AppliedHashlineEdits, FileHeader, HashlineRangeInput, LineAnchor, ParsedTaggedLine, PreparedHashlineEdit, TaggedLineMarker, TaggedLinePrefix };
|
|
34
|
+
//# sourceMappingURL=hashline.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hashline.d.cts","names":[],"sources":["../../src/types/hashline.ts"],"mappings":";UAAiB;WACN;WACA;WACA;;UAGM;WACN;WACA;;UAGM;WACN,MAAM;WACN,IAAI;WACJ;WACA,QAAQ;;UAGF;WACN;WACA,gBAAgB;;UAGV;WACN;WACA;;KAGC;UAEK;WACN;WACA;WACA,SAAS;;KAGR"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/types/hashline.d.ts
|
|
2
|
+
interface HashlineRangeInput {
|
|
3
|
+
readonly from: string;
|
|
4
|
+
readonly to: string;
|
|
5
|
+
readonly content?: string | null;
|
|
6
|
+
}
|
|
7
|
+
interface LineAnchor {
|
|
8
|
+
readonly line: number;
|
|
9
|
+
readonly hash: string;
|
|
10
|
+
}
|
|
11
|
+
interface PreparedHashlineEdit {
|
|
12
|
+
readonly from: LineAnchor;
|
|
13
|
+
readonly to: LineAnchor;
|
|
14
|
+
readonly content: string | null;
|
|
15
|
+
readonly source: HashlineRangeInput;
|
|
16
|
+
}
|
|
17
|
+
interface AppliedHashlineEdits {
|
|
18
|
+
readonly content: string;
|
|
19
|
+
readonly edits: readonly PreparedHashlineEdit[];
|
|
20
|
+
}
|
|
21
|
+
interface FileHeader {
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly tag: string;
|
|
24
|
+
}
|
|
25
|
+
type TaggedLineMarker = 'match' | 'context';
|
|
26
|
+
interface ParsedTaggedLine {
|
|
27
|
+
readonly content: string;
|
|
28
|
+
readonly line: number;
|
|
29
|
+
readonly marker?: TaggedLineMarker;
|
|
30
|
+
}
|
|
31
|
+
type TaggedLinePrefix = '' | '>> ' | ' ';
|
|
32
|
+
//#endregion
|
|
33
|
+
export { AppliedHashlineEdits, FileHeader, HashlineRangeInput, LineAnchor, ParsedTaggedLine, PreparedHashlineEdit, TaggedLineMarker, TaggedLinePrefix };
|
|
34
|
+
//# sourceMappingURL=hashline.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hashline.d.mts","names":[],"sources":["../../src/types/hashline.ts"],"mappings":";UAAiB;WACN;WACA;WACA;;UAGM;WACN;WACA;;UAGM;WACN,MAAM;WACN,IAAI;WACJ;WACA,QAAQ;;UAGF;WACN;WACA,gBAAgB;;UAGV;WACN;WACA;;KAGC;UAEK;WACN;WACA;WACA,SAAS;;KAGR"}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agimon-ai/doompi-hashline",
|
|
3
|
+
"version": "0.0.1-alpha.1",
|
|
4
|
+
"description": "Shared snapshot-bound file tags and line anchors for DoomPi tools.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"coding-agent",
|
|
8
|
+
"developer-tools",
|
|
9
|
+
"doompi",
|
|
10
|
+
"hashline",
|
|
11
|
+
"pi-package",
|
|
12
|
+
"protocol",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://agimon.ai",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Vuong Ngo",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.cjs",
|
|
23
|
+
"types": "./dist/index.d.mts",
|
|
24
|
+
"jsnext:main": "./dist/index.mjs",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.mts",
|
|
28
|
+
"import": "./dist/index.mjs",
|
|
29
|
+
"require": "./dist/index.cjs"
|
|
30
|
+
},
|
|
31
|
+
"./files": {
|
|
32
|
+
"types": "./dist/files.d.mts",
|
|
33
|
+
"import": "./dist/files.mjs",
|
|
34
|
+
"require": "./dist/files.cjs"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "26.2.0",
|
|
43
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
44
|
+
"tsdown": "0.22.14",
|
|
45
|
+
"typescript": "7.0.2",
|
|
46
|
+
"vitest": "4.1.11"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22.19.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsdown",
|
|
53
|
+
"test": "vitest --run",
|
|
54
|
+
"typecheck": "tsc --noEmit",
|
|
55
|
+
"lint": "oxlint . && oxfmt . --check",
|
|
56
|
+
"fixcode": "oxlint . --fix && oxfmt ."
|
|
57
|
+
}
|
|
58
|
+
}
|