mocode-ai 0.7.1 → 0.7.2

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.
@@ -0,0 +1,148 @@
1
+ import { normalize } from 'node:path';
2
+ import { jailResolve } from '../sandbox/index.js';
3
+ function abortError() {
4
+ const error = new Error('Resource lock acquisition aborted');
5
+ error.name = 'AbortError';
6
+ return error;
7
+ }
8
+ function requestConflicts(a, b) {
9
+ if (a.scope === 'workspace' || b.scope === 'workspace') {
10
+ if (a.mode === 'write' || b.mode === 'write')
11
+ return true;
12
+ return false;
13
+ }
14
+ return a.key === b.key && (a.mode === 'write' || b.mode === 'write');
15
+ }
16
+ function claimsConflict(a, b) {
17
+ return a.requests.some((left) => b.requests.some((right) => requestConflicts(left, right)));
18
+ }
19
+ /** Fair, abort-aware multi-resource read/write lock shared by all agent loops. */
20
+ export class ResourceLockManager {
21
+ active = new Set();
22
+ waiting = [];
23
+ acquire(requests, signal) {
24
+ if (signal?.aborted)
25
+ return Promise.reject(abortError());
26
+ const normalized = dedupeRequests(requests);
27
+ if (normalized.length === 0)
28
+ return Promise.resolve(() => undefined);
29
+ return new Promise((resolve, reject) => {
30
+ const waiter = { requests: normalized, resolve, reject, signal };
31
+ if (signal) {
32
+ waiter.onAbort = () => {
33
+ const index = this.waiting.indexOf(waiter);
34
+ if (index < 0)
35
+ return;
36
+ this.waiting.splice(index, 1);
37
+ signal.removeEventListener('abort', waiter.onAbort);
38
+ reject(abortError());
39
+ this.dispatch();
40
+ };
41
+ signal.addEventListener('abort', waiter.onAbort, { once: true });
42
+ }
43
+ this.waiting.push(waiter);
44
+ this.dispatch();
45
+ });
46
+ }
47
+ async withLocks(requests, signal, action) {
48
+ const release = await this.acquire(requests, signal);
49
+ try {
50
+ return await action();
51
+ }
52
+ finally {
53
+ release();
54
+ }
55
+ }
56
+ dispatch() {
57
+ const blocked = [];
58
+ for (let index = 0; index < this.waiting.length;) {
59
+ const waiter = this.waiting[index];
60
+ const conflictsActive = [...this.active].some((claim) => claimsConflict(waiter, claim));
61
+ const conflictsEarlier = blocked.some((claim) => claimsConflict(waiter, claim));
62
+ if (conflictsActive || conflictsEarlier) {
63
+ blocked.push(waiter);
64
+ index++;
65
+ continue;
66
+ }
67
+ this.waiting.splice(index, 1);
68
+ if (waiter.onAbort)
69
+ waiter.signal?.removeEventListener('abort', waiter.onAbort);
70
+ const claim = { requests: waiter.requests };
71
+ this.active.add(claim);
72
+ let released = false;
73
+ waiter.resolve(() => {
74
+ if (released)
75
+ return;
76
+ released = true;
77
+ this.active.delete(claim);
78
+ this.dispatch();
79
+ });
80
+ }
81
+ }
82
+ }
83
+ function dedupeRequests(requests) {
84
+ const byKey = new Map();
85
+ for (const request of requests) {
86
+ const identity = `${request.scope}:${request.key}`;
87
+ const existing = byKey.get(identity);
88
+ if (!existing || request.mode === 'write')
89
+ byKey.set(identity, request);
90
+ }
91
+ return [...byKey.values()].sort((a, b) => `${a.scope}:${a.key}`.localeCompare(`${b.scope}:${b.key}`));
92
+ }
93
+ /** Stable lock identity: sandbox realpath plus Windows case/separator normalization. */
94
+ export function canonicalFileResourceKey(input) {
95
+ let canonical = normalize(jailResolve(input));
96
+ if (process.platform === 'win32')
97
+ canonical = canonical.toLowerCase();
98
+ return `file:${canonical}`;
99
+ }
100
+ function modeFor(effect) {
101
+ return effect === 'read' ? 'read' : 'write';
102
+ }
103
+ const workspaceWrite = () => [{
104
+ key: 'workspace',
105
+ scope: 'workspace',
106
+ mode: 'write',
107
+ }];
108
+ /** Resolve declared logical resources. Any ambiguity fails closed to a workspace write lock. */
109
+ export function resolveResourceLockRequests(capabilities, args) {
110
+ if (capabilities.delegatesResourceLocks)
111
+ return [];
112
+ if (capabilities.effect === 'process' || capabilities.effect === 'unknown') {
113
+ return workspaceWrite();
114
+ }
115
+ let keys;
116
+ try {
117
+ keys = capabilities.resources?.(args) ?? [];
118
+ }
119
+ catch {
120
+ return workspaceWrite();
121
+ }
122
+ if (keys.length === 0) {
123
+ return capabilities.effect === 'network' ? [] : workspaceWrite();
124
+ }
125
+ const mode = modeFor(capabilities.effect);
126
+ const requests = [];
127
+ try {
128
+ for (const key of keys) {
129
+ if (typeof key !== 'string' || key.trim().length === 0)
130
+ return workspaceWrite();
131
+ if (key === 'workspace') {
132
+ requests.push({ key, scope: 'workspace', mode });
133
+ }
134
+ else if (key.startsWith('file:') && key.length > 5) {
135
+ requests.push({ key: canonicalFileResourceKey(key.slice(5)), scope: 'resource', mode });
136
+ }
137
+ else {
138
+ // Non-file logical resources are still lockable, but never treated as filesystem paths.
139
+ requests.push({ key, scope: 'resource', mode });
140
+ }
141
+ }
142
+ }
143
+ catch {
144
+ return workspaceWrite();
145
+ }
146
+ return dedupeRequests(requests);
147
+ }
148
+ export const toolResourceLockManager = new ResourceLockManager();
@@ -0,0 +1,108 @@
1
+ import { createRequire } from 'node:module';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ const SUPPORTED_EXTENSIONS = new Set([
6
+ '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs',
7
+ ]);
8
+ async function loadTypeScript(root) {
9
+ const candidates = [];
10
+ try {
11
+ candidates.push(createRequire(path.join(root, 'package.json')).resolve('typescript'));
12
+ }
13
+ catch {
14
+ // Target project may not depend on TypeScript; fall back to mocode's installation.
15
+ }
16
+ try {
17
+ candidates.push(createRequire(import.meta.url).resolve('typescript'));
18
+ }
19
+ catch {
20
+ // A production install may intentionally omit the optional parser.
21
+ }
22
+ for (const candidate of [...new Set(candidates)]) {
23
+ try {
24
+ const loaded = await import(pathToFileURL(candidate).href);
25
+ return loaded.default ?? loaded;
26
+ }
27
+ catch {
28
+ // Try the next resolution root.
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ function severity(category, ts) {
34
+ if (category === ts.DiagnosticCategory.Error)
35
+ return 'error';
36
+ if (category === ts.DiagnosticCategory.Warning)
37
+ return 'warning';
38
+ return 'info';
39
+ }
40
+ /** Parse only changed TS/JS files; package-wide semantic checking remains V3. */
41
+ export async function runChangedFileDiagnostics(root, changedFiles, inputFingerprint) {
42
+ const startedAt = Date.now();
43
+ const files = [...new Set(changedFiles)]
44
+ .map((file) => path.resolve(process.cwd(), file))
45
+ .filter((file) => SUPPORTED_EXTENSIONS.has(path.extname(file).toLowerCase()));
46
+ if (files.length === 0) {
47
+ return {
48
+ level: 'V1', status: 'skipped', adapter: 'typescript-parser', diagnostics: [],
49
+ output: 'No changed TypeScript or JavaScript files.', durationMs: Date.now() - startedAt,
50
+ skipReason: 'unsupported_files', inputFingerprint,
51
+ };
52
+ }
53
+ const ts = await loadTypeScript(root);
54
+ if (!ts) {
55
+ return {
56
+ level: 'V1', status: 'skipped', adapter: 'typescript-parser', diagnostics: [],
57
+ output: 'TypeScript parser is unavailable.', durationMs: Date.now() - startedAt,
58
+ skipReason: 'typescript_unavailable', inputFingerprint,
59
+ };
60
+ }
61
+ const diagnostics = [];
62
+ for (const file of files) {
63
+ let source;
64
+ try {
65
+ source = await readFile(file, 'utf8');
66
+ }
67
+ catch (error) {
68
+ diagnostics.push({
69
+ level: 'V1', source: 'typescript', severity: 'error', code: 'READ_FAILED',
70
+ file: path.relative(root, file), message: error instanceof Error ? error.message : String(error),
71
+ });
72
+ continue;
73
+ }
74
+ const result = ts.transpileModule(source, {
75
+ fileName: file,
76
+ reportDiagnostics: true,
77
+ compilerOptions: {
78
+ allowJs: true,
79
+ jsx: ts.JsxEmit.Preserve,
80
+ module: ts.ModuleKind.ESNext,
81
+ target: ts.ScriptTarget.Latest,
82
+ },
83
+ });
84
+ for (const item of result.diagnostics ?? []) {
85
+ const location = item.file && item.start !== undefined
86
+ ? item.file.getLineAndCharacterOfPosition(item.start)
87
+ : undefined;
88
+ diagnostics.push({
89
+ level: 'V1',
90
+ source: 'typescript',
91
+ severity: severity(item.category, ts),
92
+ code: item.code,
93
+ file: item.file ? path.relative(root, item.file.fileName) : path.relative(root, file),
94
+ line: location ? location.line + 1 : undefined,
95
+ column: location ? location.character + 1 : undefined,
96
+ message: ts.flattenDiagnosticMessageText(item.messageText, '\n'),
97
+ });
98
+ }
99
+ }
100
+ const failed = diagnostics.some((item) => item.severity === 'error');
101
+ const output = diagnostics.length === 0
102
+ ? `Parsed ${files.length} changed TypeScript/JavaScript file(s).`
103
+ : diagnostics.map((item) => `${item.file ?? '<unknown>'}:${item.line ?? 0}:${item.column ?? 0} TS${item.code ?? ''} ${item.message}`).join('\n');
104
+ return {
105
+ level: 'V1', status: failed ? 'failed' : 'passed', adapter: 'typescript-parser',
106
+ diagnostics, output, durationMs: Date.now() - startedAt, inputFingerprint,
107
+ };
108
+ }
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { discoverProjectProfile } from './profile.js';
4
- const SCRIPT_PRIORITY = ['typecheck', 'test', 'build'];
4
+ const COMPATIBILITY_PRIORITY = ['typecheck', 'test', 'build'];
5
+ const LAYERED_ORDER = ['typecheck', 'build', 'test'];
5
6
  const isWindows = process.platform === 'win32';
6
7
  function samePath(left, right) {
7
8
  const normalizedLeft = path.resolve(left);
@@ -10,11 +11,7 @@ function samePath(left, right) {
10
11
  ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
11
12
  : normalizedLeft === normalizedRight;
12
13
  }
13
- /** Discover one lowest-cost validation command for a package in a project profile. */
14
- export function discoverPackageValidationCommand(profile, packageProfile) {
15
- const script = SCRIPT_PRIORITY.find((name) => typeof packageProfile.scripts[name] === 'string');
16
- if (!script)
17
- return null;
14
+ function commandFor(profile, packageProfile, script) {
18
15
  return {
19
16
  script,
20
17
  command: `${profile.packageManager} run ${script}`,
@@ -22,6 +19,17 @@ export function discoverPackageValidationCommand(profile, packageProfile) {
22
19
  cwd: packageProfile.root,
23
20
  };
24
21
  }
22
+ /** Discover every available V3 command in increasing-cost order. */
23
+ export function discoverPackageValidationCommands(profile, packageProfile) {
24
+ return LAYERED_ORDER
25
+ .filter((script) => typeof packageProfile.scripts[script] === 'string')
26
+ .map((script) => commandFor(profile, packageProfile, script));
27
+ }
28
+ /** Compatibility API retained for callers that intentionally want one command. */
29
+ export function discoverPackageValidationCommand(profile, packageProfile) {
30
+ const script = COMPATIBILITY_PRIORITY.find((name) => typeof packageProfile.scripts[name] === 'string');
31
+ return script ? commandFor(profile, packageProfile, script) : null;
32
+ }
25
33
  /** Compatibility wrapper that discovers the root package validation command. */
26
34
  export function discoverValidationCommand(root) {
27
35
  const resolvedRoot = path.resolve(root);
@@ -0,0 +1,54 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, statSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ function sha256(value) {
5
+ return createHash('sha256').update(value).digest('hex');
6
+ }
7
+ function normalizeText(value, root) {
8
+ const normalizedRoot = path.resolve(root).replace(/\\/g, '/');
9
+ return value
10
+ .replace(/\u001b\[[0-9;]*m/g, '')
11
+ .replace(/\\/g, '/')
12
+ .replaceAll(normalizedRoot, '<root>')
13
+ .replace(/\r\n?/g, '\n')
14
+ .trim();
15
+ }
16
+ /** Fingerprint the relevant on-disk inputs, so rewriting identical content can reuse validation. */
17
+ export function fingerprintFiles(root, files) {
18
+ const entries = [...new Set(files)].sort().map((file) => {
19
+ const absolute = path.resolve(process.cwd(), file);
20
+ const display = path.relative(root, absolute).replace(/\\/g, '/');
21
+ try {
22
+ const stat = statSync(absolute);
23
+ if (!stat.isFile())
24
+ return `${display}\0<${stat.isDirectory() ? 'directory' : 'other'}>`;
25
+ return `${display}\0${sha256(readFileSync(absolute))}`;
26
+ }
27
+ catch {
28
+ return `${display}\0<missing>`;
29
+ }
30
+ });
31
+ return sha256(entries.join('\n'));
32
+ }
33
+ export function fingerprintValidation(input) {
34
+ const diagnostics = [...input.diagnostics]
35
+ .map((item) => ({
36
+ source: item.source,
37
+ severity: item.severity,
38
+ code: item.code ?? '',
39
+ file: item.file ? normalizeText(item.file, input.root) : '',
40
+ line: item.line ?? 0,
41
+ column: item.column ?? 0,
42
+ message: normalizeText(item.message, input.root),
43
+ packageName: item.packageName ?? '',
44
+ }))
45
+ .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
46
+ return sha256(JSON.stringify({
47
+ level: input.level ?? '',
48
+ status: input.status,
49
+ adapter: input.adapter ?? '',
50
+ command: input.command ? normalizeText(input.command, input.root) : '',
51
+ diagnostics,
52
+ output: diagnostics.length === 0 ? normalizeText(input.output ?? '', input.root) : '',
53
+ }));
54
+ }