preclaim 0.1.0 → 0.1.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/package.json +6 -3
- package/.turbo/turbo-build.log +0 -4
- package/src/commands/check.ts +0 -28
- package/src/commands/config.ts +0 -43
- package/src/commands/init.ts +0 -109
- package/src/commands/install-hooks.ts +0 -72
- package/src/commands/lock.ts +0 -30
- package/src/commands/login.ts +0 -120
- package/src/commands/status.ts +0 -15
- package/src/commands/unlock.ts +0 -25
- package/src/commands/whoami.ts +0 -15
- package/src/hooks/heartbeat-daemon.ts +0 -49
- package/src/hooks/post-tool-use.ts +0 -44
- package/src/hooks/pre-tool-use.ts +0 -110
- package/src/hooks/session-start.ts +0 -87
- package/src/hooks/stop.ts +0 -43
- package/src/index.ts +0 -74
- package/src/lib/auth.ts +0 -17
- package/src/lib/client-factory.ts +0 -26
- package/src/lib/hook-io.ts +0 -37
- package/src/lib/output.ts +0 -20
- package/tsconfig.json +0 -8
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "preclaim",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "AI File Coordination Layer — predictive file locking for AI coding agents",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"bin": {
|
|
6
|
-
"preclaim": "
|
|
7
|
+
"preclaim": "dist/index.js"
|
|
7
8
|
},
|
|
8
|
-
"
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
9
12
|
"scripts": {
|
|
10
13
|
"build": "tsc",
|
|
11
14
|
"dev": "tsc --watch",
|
package/.turbo/turbo-build.log
DELETED
package/src/commands/check.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { resolveContext } from '../lib/client-factory.js';
|
|
2
|
-
|
|
3
|
-
export async function checkCommand(filePaths: string[]) {
|
|
4
|
-
if (filePaths.length === 0) {
|
|
5
|
-
console.error('Specify one or more file paths to check.');
|
|
6
|
-
process.exit(1);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const { client, config } = await resolveContext();
|
|
10
|
-
|
|
11
|
-
const result = await client.batchCheck({
|
|
12
|
-
project_id: config.projectId,
|
|
13
|
-
file_paths: filePaths,
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
if (result.error) {
|
|
17
|
-
console.error(`Failed to check: ${result.error}`);
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
for (const [path, lock] of Object.entries(result.data!.locks)) {
|
|
22
|
-
if (lock) {
|
|
23
|
-
console.log(`LOCKED ${path} (session: ${lock.session_id.slice(0, 8)}… expires: ${new Date(lock.expires_at).toLocaleTimeString()})`);
|
|
24
|
-
} else {
|
|
25
|
-
console.log(`FREE ${path}`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
package/src/commands/config.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { findConfig, type PreclaimConfig } from '@preclaim/core';
|
|
3
|
-
|
|
4
|
-
export async function configCommand(opts: { get?: string; set?: string }) {
|
|
5
|
-
const found = await findConfig();
|
|
6
|
-
if (!found) {
|
|
7
|
-
console.error('No .preclaim.json found. Run `preclaim init` first.');
|
|
8
|
-
process.exit(1);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
if (opts.get) {
|
|
12
|
-
const value = found.config[opts.get as keyof PreclaimConfig];
|
|
13
|
-
console.log(JSON.stringify(value, null, 2));
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (opts.set) {
|
|
18
|
-
const [key, ...rest] = opts.set.split('=');
|
|
19
|
-
const value = rest.join('=');
|
|
20
|
-
|
|
21
|
-
if (!key || !value) {
|
|
22
|
-
console.error('Usage: preclaim config --set key=value');
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const raw = await readFile(found.configPath, 'utf-8');
|
|
27
|
-
const config = JSON.parse(raw) as Record<string, unknown>;
|
|
28
|
-
|
|
29
|
-
// Try to parse as JSON, fallback to string
|
|
30
|
-
try {
|
|
31
|
-
config[key] = JSON.parse(value);
|
|
32
|
-
} catch {
|
|
33
|
-
config[key] = value;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
await writeFile(found.configPath, JSON.stringify(config, null, 2) + '\n');
|
|
37
|
-
console.log(`Set ${key} = ${JSON.stringify(config[key])}`);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// No flags — show full config
|
|
42
|
-
console.log(JSON.stringify(found.config, null, 2));
|
|
43
|
-
}
|
package/src/commands/init.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { writeFile, readFile } from 'node:fs/promises';
|
|
2
|
-
import { join, basename } from 'node:path';
|
|
3
|
-
import { defaultConfig, loadCredentials, PreclaimClient } from '@preclaim/core';
|
|
4
|
-
import { loginCommand } from './login.js';
|
|
5
|
-
|
|
6
|
-
const DEFAULT_BACKEND = 'https://preclaim.dev';
|
|
7
|
-
|
|
8
|
-
export async function initCommand(opts: { backend?: string; projectId?: string }) {
|
|
9
|
-
const configPath = join(process.cwd(), '.preclaim.json');
|
|
10
|
-
|
|
11
|
-
// Check if already exists
|
|
12
|
-
try {
|
|
13
|
-
await readFile(configPath, 'utf-8');
|
|
14
|
-
console.log('.preclaim.json already exists. Use `preclaim config` to modify.');
|
|
15
|
-
return;
|
|
16
|
-
} catch {
|
|
17
|
-
// File doesn't exist, proceed
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const backend = opts.backend ?? DEFAULT_BACKEND;
|
|
21
|
-
|
|
22
|
-
// If project ID provided directly, skip onboarding
|
|
23
|
-
if (opts.projectId) {
|
|
24
|
-
const config = defaultConfig(opts.projectId, backend);
|
|
25
|
-
await writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
26
|
-
console.log(`Created .preclaim.json (project: ${opts.projectId})`);
|
|
27
|
-
printNextSteps();
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Check if logged in, if not: login first
|
|
32
|
-
let creds = await loadCredentials();
|
|
33
|
-
if (!creds) {
|
|
34
|
-
console.log('Not logged in. Starting authentication...\n');
|
|
35
|
-
await loginCommand();
|
|
36
|
-
creds = await loadCredentials();
|
|
37
|
-
if (!creds) {
|
|
38
|
-
console.error('Login failed. Please try again.');
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
console.log('');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Derive project name from directory
|
|
45
|
-
const dirName = basename(process.cwd());
|
|
46
|
-
const projectSlug = dirName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
47
|
-
const projectName = dirName;
|
|
48
|
-
|
|
49
|
-
console.log(`Setting up Preclaim for "${projectName}"...`);
|
|
50
|
-
|
|
51
|
-
// Call onboard API
|
|
52
|
-
const client = new PreclaimClient({
|
|
53
|
-
baseUrl: backend,
|
|
54
|
-
accessToken: creds.accessToken,
|
|
55
|
-
timeoutMs: 10000,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const res = await fetch(`${backend}/api/v1/onboard`, {
|
|
59
|
-
method: 'POST',
|
|
60
|
-
headers: {
|
|
61
|
-
'Content-Type': 'application/json',
|
|
62
|
-
'Authorization': `Bearer ${creds.accessToken}`,
|
|
63
|
-
},
|
|
64
|
-
body: JSON.stringify({
|
|
65
|
-
project_name: projectName,
|
|
66
|
-
project_slug: projectSlug,
|
|
67
|
-
}),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
if (!res.ok) {
|
|
71
|
-
const body = await res.text();
|
|
72
|
-
console.error(`Failed to create project: ${body}`);
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const { data } = await res.json() as { data: { project_id: string; org_id: string; already_existed: boolean } };
|
|
77
|
-
|
|
78
|
-
if (data.already_existed) {
|
|
79
|
-
console.log(`Project "${projectName}" already exists, using existing project.`);
|
|
80
|
-
} else {
|
|
81
|
-
console.log(`Project "${projectName}" created.`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Update credentials with org_id
|
|
85
|
-
if (creds.user.orgId !== data.org_id) {
|
|
86
|
-
creds.user.orgId = data.org_id;
|
|
87
|
-
const { getCredentialsPath } = await import('@preclaim/core');
|
|
88
|
-
const { writeFile: wf, mkdir } = await import('node:fs/promises');
|
|
89
|
-
const { dirname } = await import('node:path');
|
|
90
|
-
const credPath = getCredentialsPath();
|
|
91
|
-
await mkdir(dirname(credPath), { recursive: true });
|
|
92
|
-
await wf(credPath, JSON.stringify(creds, null, 2) + '\n', { mode: 0o600 });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Write config
|
|
96
|
-
const config = defaultConfig(data.project_id, backend);
|
|
97
|
-
await writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
98
|
-
|
|
99
|
-
console.log(`\nCreated .preclaim.json`);
|
|
100
|
-
printNextSteps();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function printNextSteps() {
|
|
104
|
-
console.log('');
|
|
105
|
-
console.log('Next steps:');
|
|
106
|
-
console.log(' 1. preclaim install-hooks');
|
|
107
|
-
console.log(' 2. Commit .preclaim.json to your repo');
|
|
108
|
-
console.log(' 3. Open multiple Claude Code terminals — locks are automatic');
|
|
109
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
-
import { join, dirname } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
|
|
7
|
-
interface ClaudeSettings {
|
|
8
|
-
hooks?: Record<string, Array<{
|
|
9
|
-
matcher: string;
|
|
10
|
-
hooks: Array<{
|
|
11
|
-
type: string;
|
|
12
|
-
command: string;
|
|
13
|
-
}>;
|
|
14
|
-
}>>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function installHooksCommand() {
|
|
18
|
-
const hooksDir = join(__dirname, '..', 'hooks');
|
|
19
|
-
const settingsDir = join(process.cwd(), '.claude');
|
|
20
|
-
const settingsPath = join(settingsDir, 'settings.json');
|
|
21
|
-
|
|
22
|
-
// Read existing settings or create new
|
|
23
|
-
let settings: ClaudeSettings = {};
|
|
24
|
-
try {
|
|
25
|
-
const raw = await readFile(settingsPath, 'utf-8');
|
|
26
|
-
settings = JSON.parse(raw);
|
|
27
|
-
} catch {
|
|
28
|
-
// File doesn't exist
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
settings.hooks = {
|
|
32
|
-
PreToolUse: [{
|
|
33
|
-
matcher: '',
|
|
34
|
-
hooks: [{
|
|
35
|
-
type: 'command',
|
|
36
|
-
command: `node ${join(hooksDir, 'pre-tool-use.js')}`,
|
|
37
|
-
}],
|
|
38
|
-
}],
|
|
39
|
-
PostToolUse: [{
|
|
40
|
-
matcher: '',
|
|
41
|
-
hooks: [{
|
|
42
|
-
type: 'command',
|
|
43
|
-
command: `node ${join(hooksDir, 'post-tool-use.js')}`,
|
|
44
|
-
}],
|
|
45
|
-
}],
|
|
46
|
-
Stop: [{
|
|
47
|
-
matcher: '',
|
|
48
|
-
hooks: [{
|
|
49
|
-
type: 'command',
|
|
50
|
-
command: `node ${join(hooksDir, 'stop.js')}`,
|
|
51
|
-
}],
|
|
52
|
-
}],
|
|
53
|
-
SessionStart: [{
|
|
54
|
-
matcher: '',
|
|
55
|
-
hooks: [{
|
|
56
|
-
type: 'command',
|
|
57
|
-
command: `node ${join(hooksDir, 'session-start.js')}`,
|
|
58
|
-
}],
|
|
59
|
-
}],
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
await mkdir(settingsDir, { recursive: true });
|
|
63
|
-
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
64
|
-
|
|
65
|
-
console.log('Claude Code hooks installed in .claude/settings.json');
|
|
66
|
-
console.log('');
|
|
67
|
-
console.log('Hooks configured:');
|
|
68
|
-
console.log(' - PreToolUse: file lock gatekeeper');
|
|
69
|
-
console.log(' - PostToolUse: commit detection → release locks');
|
|
70
|
-
console.log(' - Stop: session cleanup');
|
|
71
|
-
console.log(' - SessionStart: session registration + heartbeat');
|
|
72
|
-
}
|
package/src/commands/lock.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { resolveContext } from '../lib/client-factory.js';
|
|
2
|
-
|
|
3
|
-
export async function lockCommand(filePath: string, opts: { session?: string; ttl?: string }) {
|
|
4
|
-
const { client, config, credentials } = await resolveContext();
|
|
5
|
-
const sessionId = opts.session ?? `manual_${crypto.randomUUID().slice(0, 8)}`;
|
|
6
|
-
const ttl = opts.ttl ? parseInt(opts.ttl, 10) : config.ttl;
|
|
7
|
-
|
|
8
|
-
const result = await client.claimFile({
|
|
9
|
-
project_id: config.projectId,
|
|
10
|
-
file_path: filePath,
|
|
11
|
-
session_id: sessionId,
|
|
12
|
-
ttl_minutes: ttl,
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
if (result.error) {
|
|
16
|
-
console.error(`Failed to lock: ${result.error}`);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const data = result.data!;
|
|
21
|
-
|
|
22
|
-
if (data.status === 'acquired' || data.status === 'already_held') {
|
|
23
|
-
console.log(`Locked: ${filePath} (expires: ${new Date(data.expires_at!).toLocaleTimeString()})`);
|
|
24
|
-
} else if (data.status === 'conflict') {
|
|
25
|
-
console.error(`Conflict: ${filePath} is locked by session ${data.holder!.session_id.slice(0, 8)}…`);
|
|
26
|
-
console.error(` Acquired: ${new Date(data.holder!.acquired_at).toLocaleTimeString()}`);
|
|
27
|
-
console.error(` Expires: ${new Date(data.holder!.expires_at).toLocaleTimeString()}`);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
}
|
package/src/commands/login.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { createServer, type Server } from 'node:http';
|
|
2
|
-
import { writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
-
import { dirname } from 'node:path';
|
|
4
|
-
import { getCredentialsPath, findConfig } from '@preclaim/core';
|
|
5
|
-
|
|
6
|
-
const SUPABASE_URL = 'https://aawbukcvngdffueowjsa.supabase.co';
|
|
7
|
-
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFhd2J1a2N2bmdkZmZ1ZW93anNhIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzM3NjI2NTcsImV4cCI6MjA4OTMzODY1N30.pwAyjgnbdoZmmJdsG2jF0nbvT4hueb8UZvstsdYhFFs';
|
|
8
|
-
|
|
9
|
-
export async function loginCommand() {
|
|
10
|
-
const server = createServer();
|
|
11
|
-
|
|
12
|
-
const port = await new Promise<number>((resolve) => {
|
|
13
|
-
server.listen(0, () => {
|
|
14
|
-
resolve((server.address() as { port: number }).port);
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const redirectTo = `http://localhost:${port}/callback`;
|
|
19
|
-
const oauthUrl = `${SUPABASE_URL}/auth/v1/authorize?provider=github&redirect_to=${encodeURIComponent(redirectTo)}`;
|
|
20
|
-
|
|
21
|
-
console.log('Opening browser for GitHub authentication...');
|
|
22
|
-
console.log(`If browser doesn't open, visit:\n${oauthUrl}\n`);
|
|
23
|
-
|
|
24
|
-
const { exec } = await import('node:child_process');
|
|
25
|
-
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
26
|
-
exec(`${openCmd} "${oauthUrl}"`);
|
|
27
|
-
|
|
28
|
-
await handleAuthCallback(server);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function handleAuthCallback(server: Server): Promise<void> {
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
server.on('request', async (req, res) => {
|
|
34
|
-
const url = new URL(req.url!, `http://localhost`);
|
|
35
|
-
|
|
36
|
-
if (url.pathname === '/callback') {
|
|
37
|
-
// Supabase implicit flow: tokens come in hash fragment.
|
|
38
|
-
// Serve a page that extracts them and POSTs to /token.
|
|
39
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
40
|
-
res.end(`<!DOCTYPE html><html><body>
|
|
41
|
-
<h1>Logging in to Preclaim...</h1>
|
|
42
|
-
<script>
|
|
43
|
-
const hash = window.location.hash.substring(1);
|
|
44
|
-
const params = new URLSearchParams(hash);
|
|
45
|
-
const access_token = params.get('access_token');
|
|
46
|
-
const refresh_token = params.get('refresh_token');
|
|
47
|
-
const expires_in = params.get('expires_in');
|
|
48
|
-
if (access_token) {
|
|
49
|
-
fetch('/token', {
|
|
50
|
-
method: 'POST',
|
|
51
|
-
headers: { 'Content-Type': 'application/json' },
|
|
52
|
-
body: JSON.stringify({ access_token, refresh_token, expires_in })
|
|
53
|
-
}).then(() => {
|
|
54
|
-
document.body.innerHTML = '<h1>Logged in to Preclaim!</h1><p>You can close this tab.</p>';
|
|
55
|
-
});
|
|
56
|
-
} else {
|
|
57
|
-
document.body.innerHTML = '<h1>Login failed</h1><p>No token received. Check the URL.</p>';
|
|
58
|
-
}
|
|
59
|
-
</script></body></html>`);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (url.pathname === '/token' && req.method === 'POST') {
|
|
64
|
-
let body = '';
|
|
65
|
-
req.on('data', (chunk: Buffer) => { body += chunk; });
|
|
66
|
-
req.on('end', async () => {
|
|
67
|
-
try {
|
|
68
|
-
const { access_token, refresh_token, expires_in } = JSON.parse(body);
|
|
69
|
-
|
|
70
|
-
// Get user info
|
|
71
|
-
const userRes = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
|
|
72
|
-
headers: {
|
|
73
|
-
'Authorization': `Bearer ${access_token}`,
|
|
74
|
-
'apikey': SUPABASE_ANON_KEY,
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
if (!userRes.ok) throw new Error(`Failed to get user info: ${userRes.status}`);
|
|
79
|
-
|
|
80
|
-
const user = await userRes.json() as { id: string; email?: string };
|
|
81
|
-
|
|
82
|
-
// Save credentials
|
|
83
|
-
const credPath = getCredentialsPath();
|
|
84
|
-
await mkdir(dirname(credPath), { recursive: true });
|
|
85
|
-
await writeFile(credPath, JSON.stringify({
|
|
86
|
-
accessToken: access_token,
|
|
87
|
-
refreshToken: refresh_token,
|
|
88
|
-
expiresAt: new Date(Date.now() + parseInt(expires_in) * 1000).toISOString(),
|
|
89
|
-
user: {
|
|
90
|
-
id: user.id,
|
|
91
|
-
email: user.email ?? '',
|
|
92
|
-
orgId: '',
|
|
93
|
-
},
|
|
94
|
-
}, null, 2) + '\n', { mode: 0o600 });
|
|
95
|
-
|
|
96
|
-
res.writeHead(200);
|
|
97
|
-
res.end('ok');
|
|
98
|
-
|
|
99
|
-
console.log(`Logged in as ${user.email ?? user.id}`);
|
|
100
|
-
server.close();
|
|
101
|
-
resolve();
|
|
102
|
-
} catch (err) {
|
|
103
|
-
res.writeHead(500);
|
|
104
|
-
res.end('error');
|
|
105
|
-
console.error('Login failed:', err);
|
|
106
|
-
server.close();
|
|
107
|
-
reject(err);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
setTimeout(() => {
|
|
115
|
-
console.error('Login timed out.');
|
|
116
|
-
server.close();
|
|
117
|
-
process.exit(1);
|
|
118
|
-
}, 120_000);
|
|
119
|
-
});
|
|
120
|
-
}
|
package/src/commands/status.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { resolveContext } from '../lib/client-factory.js';
|
|
2
|
-
import { formatLockTable } from '../lib/output.js';
|
|
3
|
-
|
|
4
|
-
export async function statusCommand() {
|
|
5
|
-
const { client, config } = await resolveContext();
|
|
6
|
-
|
|
7
|
-
const result = await client.listLocks(config.projectId);
|
|
8
|
-
|
|
9
|
-
if (result.error) {
|
|
10
|
-
console.error(`Failed to fetch status: ${result.error}`);
|
|
11
|
-
process.exit(1);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
console.log(formatLockTable(result.data!));
|
|
15
|
-
}
|
package/src/commands/unlock.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { resolveContext } from '../lib/client-factory.js';
|
|
2
|
-
|
|
3
|
-
export async function unlockCommand(filePath: string | undefined, opts: { session?: string; all?: boolean }) {
|
|
4
|
-
const { client, config } = await resolveContext();
|
|
5
|
-
|
|
6
|
-
if (!filePath && !opts.all) {
|
|
7
|
-
console.error('Specify a file path or use --all to release all locks.');
|
|
8
|
-
process.exit(1);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const sessionId = opts.session ?? `manual_${crypto.randomUUID().slice(0, 8)}`;
|
|
12
|
-
|
|
13
|
-
const result = await client.releaseLocks({
|
|
14
|
-
project_id: config.projectId,
|
|
15
|
-
file_path: filePath,
|
|
16
|
-
session_id: sessionId,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
if (result.error) {
|
|
20
|
-
console.error(`Failed to unlock: ${result.error}`);
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
console.log(`Released ${result.data!.released} lock(s).`);
|
|
25
|
-
}
|
package/src/commands/whoami.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { loadCredentials } from '@preclaim/core';
|
|
2
|
-
|
|
3
|
-
export async function whoamiCommand() {
|
|
4
|
-
const creds = await loadCredentials();
|
|
5
|
-
|
|
6
|
-
if (!creds) {
|
|
7
|
-
console.log('Not logged in. Run `preclaim login` to authenticate.');
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
console.log(`Email: ${creds.user.email}`);
|
|
12
|
-
console.log(`User: ${creds.user.id}`);
|
|
13
|
-
console.log(`Org: ${creds.user.orgId ?? 'none'}`);
|
|
14
|
-
console.log(`Expires: ${new Date(creds.expiresAt).toLocaleString()}`);
|
|
15
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Heartbeat daemon — runs as detached background process
|
|
3
|
-
// Extends TTL on all session locks every 60s
|
|
4
|
-
|
|
5
|
-
import { PreclaimClient } from '@preclaim/core';
|
|
6
|
-
|
|
7
|
-
const sessionId = process.env.PRECLAIM_SESSION_ID;
|
|
8
|
-
const backend = process.env.PRECLAIM_BACKEND;
|
|
9
|
-
const accessToken = process.env.PRECLAIM_ACCESS_TOKEN;
|
|
10
|
-
|
|
11
|
-
if (!sessionId || !backend || !accessToken) {
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const client = new PreclaimClient({
|
|
16
|
-
baseUrl: backend,
|
|
17
|
-
accessToken,
|
|
18
|
-
timeoutMs: 5000,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const INTERVAL_MS = 60_000;
|
|
22
|
-
const MAX_FAILURES = 5;
|
|
23
|
-
let failures = 0;
|
|
24
|
-
|
|
25
|
-
async function heartbeat() {
|
|
26
|
-
const result = await client.heartbeat({ session_id: sessionId! });
|
|
27
|
-
|
|
28
|
-
if (result.error) {
|
|
29
|
-
failures++;
|
|
30
|
-
if (failures >= MAX_FAILURES) {
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
} else {
|
|
34
|
-
failures = 0;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Run immediately, then every 60s
|
|
39
|
-
heartbeat();
|
|
40
|
-
const interval = setInterval(heartbeat, INTERVAL_MS);
|
|
41
|
-
|
|
42
|
-
// Cleanup on signals
|
|
43
|
-
function shutdown() {
|
|
44
|
-
clearInterval(interval);
|
|
45
|
-
process.exit(0);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
process.on('SIGTERM', shutdown);
|
|
49
|
-
process.on('SIGINT', shutdown);
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// PostToolUse hook — commit detection
|
|
3
|
-
// Detects git commit commands and releases all session locks
|
|
4
|
-
|
|
5
|
-
import { PreclaimClient, findConfig, loadCredentials } from '@preclaim/core';
|
|
6
|
-
import { readHookInput } from '../lib/hook-io.js';
|
|
7
|
-
|
|
8
|
-
async function main() {
|
|
9
|
-
try {
|
|
10
|
-
const input = await readHookInput();
|
|
11
|
-
|
|
12
|
-
// Only match Bash tool calls
|
|
13
|
-
if (input.tool_name !== 'Bash') return;
|
|
14
|
-
|
|
15
|
-
const command = input.tool_input?.command as string | undefined;
|
|
16
|
-
if (!command) return;
|
|
17
|
-
|
|
18
|
-
// Detect git commit (not amend, not just `git commit --help`)
|
|
19
|
-
const isCommit = /\bgit\s+commit\b/.test(command) && !/--help/.test(command);
|
|
20
|
-
if (!isCommit) return;
|
|
21
|
-
|
|
22
|
-
const found = await findConfig();
|
|
23
|
-
if (!found) return;
|
|
24
|
-
|
|
25
|
-
const creds = await loadCredentials();
|
|
26
|
-
if (!creds) return;
|
|
27
|
-
|
|
28
|
-
const client = new PreclaimClient({
|
|
29
|
-
baseUrl: found.config.backend,
|
|
30
|
-
accessToken: creds.accessToken,
|
|
31
|
-
timeoutMs: 3000,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// Release all locks for this session
|
|
35
|
-
await client.releaseLocks({
|
|
36
|
-
project_id: found.config.projectId,
|
|
37
|
-
session_id: input.session_id,
|
|
38
|
-
});
|
|
39
|
-
} catch {
|
|
40
|
-
// Silent fail — non-critical
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
main();
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// PreToolUse hook — the gatekeeper
|
|
3
|
-
// Intercepts Edit/Write/MultiEdit tool calls, claims file locks
|
|
4
|
-
|
|
5
|
-
import { PreclaimClient, findConfig, loadCredentials } from '@preclaim/core';
|
|
6
|
-
import { readHookInput, writeHookOutput } from '../lib/hook-io.js';
|
|
7
|
-
import { minimatch } from 'minimatch';
|
|
8
|
-
|
|
9
|
-
const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
10
|
-
|
|
11
|
-
async function main() {
|
|
12
|
-
try {
|
|
13
|
-
const input = await readHookInput();
|
|
14
|
-
|
|
15
|
-
// Only intercept file-writing tools
|
|
16
|
-
if (!input.tool_name || !WRITE_TOOLS.has(input.tool_name)) {
|
|
17
|
-
return; // No output = allow
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Extract file path from tool input
|
|
21
|
-
const filePath = input.tool_input?.file_path as string | undefined;
|
|
22
|
-
if (!filePath) {
|
|
23
|
-
return; // No file path = allow
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Load config
|
|
27
|
-
const found = await findConfig();
|
|
28
|
-
if (!found) {
|
|
29
|
-
return; // No config = allow (not a preclaim project)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Check ignore patterns
|
|
33
|
-
const relativePath = filePath.startsWith('/') ? filePath : filePath;
|
|
34
|
-
if (found.config.ignore.some(pattern => minimatch(relativePath, pattern))) {
|
|
35
|
-
return; // Ignored file = allow
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Load credentials
|
|
39
|
-
const creds = await loadCredentials();
|
|
40
|
-
if (!creds) {
|
|
41
|
-
if (found.config.failOpen) return;
|
|
42
|
-
writeHookOutput({
|
|
43
|
-
permissionDecision: 'deny',
|
|
44
|
-
reason: 'Preclaim: not authenticated. Run `preclaim login`.',
|
|
45
|
-
});
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Claim file
|
|
50
|
-
const client = new PreclaimClient({
|
|
51
|
-
baseUrl: found.config.backend,
|
|
52
|
-
accessToken: creds.accessToken,
|
|
53
|
-
timeoutMs: 2000, // Must be fast
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const result = await client.claimFile({
|
|
57
|
-
project_id: found.config.projectId,
|
|
58
|
-
file_path: relativePath,
|
|
59
|
-
session_id: input.session_id,
|
|
60
|
-
ttl_minutes: found.config.ttl,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Network error — fail open
|
|
64
|
-
if (result.error) {
|
|
65
|
-
if (found.config.failOpen) {
|
|
66
|
-
writeHookOutput({
|
|
67
|
-
permissionDecision: 'allow',
|
|
68
|
-
systemMessage: `[Preclaim] Warning: could not reach server (${result.error}). Proceeding without lock.`,
|
|
69
|
-
});
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
writeHookOutput({
|
|
73
|
-
permissionDecision: 'deny',
|
|
74
|
-
reason: `Preclaim: server error — ${result.error}`,
|
|
75
|
-
});
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const data = result.data!;
|
|
80
|
-
|
|
81
|
-
if (data.status === 'acquired') {
|
|
82
|
-
writeHookOutput({
|
|
83
|
-
permissionDecision: 'allow',
|
|
84
|
-
systemMessage: `[Preclaim] Locked: ${relativePath} (expires: ${data.expires_at})`,
|
|
85
|
-
});
|
|
86
|
-
} else if (data.status === 'already_held') {
|
|
87
|
-
writeHookOutput({
|
|
88
|
-
permissionDecision: 'allow',
|
|
89
|
-
systemMessage: `[Preclaim] Lock extended: ${relativePath} (expires: ${data.expires_at})`,
|
|
90
|
-
});
|
|
91
|
-
} else if (data.status === 'conflict') {
|
|
92
|
-
writeHookOutput({
|
|
93
|
-
permissionDecision: 'deny',
|
|
94
|
-
reason: [
|
|
95
|
-
`Preclaim: ${relativePath} is locked by another session.`,
|
|
96
|
-
` Session: ${data.holder!.session_id.slice(0, 8)}…`,
|
|
97
|
-
` Since: ${new Date(data.holder!.acquired_at).toLocaleTimeString()}`,
|
|
98
|
-
` Expires: ${new Date(data.holder!.expires_at).toLocaleTimeString()}`,
|
|
99
|
-
'',
|
|
100
|
-
'Wait for the lock to expire or work on a different file.',
|
|
101
|
-
].join('\n'),
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
} catch {
|
|
105
|
-
// Fail open — never block development
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
main();
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SessionStart hook — init
|
|
3
|
-
// Registers session, starts heartbeat daemon, injects system message
|
|
4
|
-
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
6
|
-
import { writeFile } from 'node:fs/promises';
|
|
7
|
-
import { join, dirname } from 'node:path';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { PreclaimClient, findConfig, loadCredentials } from '@preclaim/core';
|
|
10
|
-
import { readHookInput, writeHookOutput } from '../lib/hook-io.js';
|
|
11
|
-
|
|
12
|
-
async function main() {
|
|
13
|
-
try {
|
|
14
|
-
const input = await readHookInput();
|
|
15
|
-
|
|
16
|
-
const found = await findConfig();
|
|
17
|
-
if (!found) return;
|
|
18
|
-
|
|
19
|
-
const creds = await loadCredentials();
|
|
20
|
-
if (!creds) {
|
|
21
|
-
writeHookOutput({
|
|
22
|
-
systemMessage: '[Preclaim] Not authenticated. File locking disabled. Run `preclaim login` to enable.',
|
|
23
|
-
});
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const client = new PreclaimClient({
|
|
28
|
-
baseUrl: found.config.backend,
|
|
29
|
-
accessToken: creds.accessToken,
|
|
30
|
-
timeoutMs: 3000,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Register session
|
|
34
|
-
const result = await client.registerSession({
|
|
35
|
-
session_id: input.session_id,
|
|
36
|
-
project_id: found.config.projectId,
|
|
37
|
-
provider: 'claude-code',
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (result.error) {
|
|
41
|
-
if (found.config.failOpen) {
|
|
42
|
-
writeHookOutput({
|
|
43
|
-
systemMessage: `[Preclaim] Warning: could not register session (${result.error}). File locking may not work.`,
|
|
44
|
-
});
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Start heartbeat daemon
|
|
50
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
51
|
-
const heartbeatScript = join(__dirname, 'heartbeat-daemon.js');
|
|
52
|
-
|
|
53
|
-
const daemon = spawn('node', [heartbeatScript], {
|
|
54
|
-
detached: true,
|
|
55
|
-
stdio: 'ignore',
|
|
56
|
-
env: {
|
|
57
|
-
...process.env,
|
|
58
|
-
PRECLAIM_SESSION_ID: input.session_id,
|
|
59
|
-
PRECLAIM_BACKEND: found.config.backend,
|
|
60
|
-
PRECLAIM_ACCESS_TOKEN: creds.accessToken,
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
daemon.unref();
|
|
65
|
-
|
|
66
|
-
// Save PID for cleanup
|
|
67
|
-
if (daemon.pid) {
|
|
68
|
-
await writeFile(join(process.cwd(), '.preclaim.pid'), String(daemon.pid));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
writeHookOutput({
|
|
72
|
-
systemMessage: [
|
|
73
|
-
'[Preclaim] Session registered. File locking is active.',
|
|
74
|
-
'',
|
|
75
|
-
'Preclaim coordinates file access across multiple AI sessions:',
|
|
76
|
-
'- Files are automatically locked when you edit them',
|
|
77
|
-
'- Locks prevent other sessions from editing the same files',
|
|
78
|
-
'- Locks are released when you commit or this session ends',
|
|
79
|
-
'- If a lock is denied, work on a different file',
|
|
80
|
-
].join('\n'),
|
|
81
|
-
});
|
|
82
|
-
} catch {
|
|
83
|
-
// Fail open
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
main();
|
package/src/hooks/stop.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Stop hook — cleanup
|
|
3
|
-
// Releases all locks and stops heartbeat daemon
|
|
4
|
-
|
|
5
|
-
import { readFile, unlink } from 'node:fs/promises';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { PreclaimClient, findConfig, loadCredentials } from '@preclaim/core';
|
|
8
|
-
import { readHookInput } from '../lib/hook-io.js';
|
|
9
|
-
|
|
10
|
-
async function main() {
|
|
11
|
-
try {
|
|
12
|
-
const input = await readHookInput();
|
|
13
|
-
|
|
14
|
-
const found = await findConfig();
|
|
15
|
-
if (!found) return;
|
|
16
|
-
|
|
17
|
-
const creds = await loadCredentials();
|
|
18
|
-
if (!creds) return;
|
|
19
|
-
|
|
20
|
-
const client = new PreclaimClient({
|
|
21
|
-
baseUrl: found.config.backend,
|
|
22
|
-
accessToken: creds.accessToken,
|
|
23
|
-
timeoutMs: 3000,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// End session (releases all locks)
|
|
27
|
-
await client.endSession(input.session_id);
|
|
28
|
-
|
|
29
|
-
// Kill heartbeat daemon if running
|
|
30
|
-
const pidFile = join(process.cwd(), '.preclaim.pid');
|
|
31
|
-
try {
|
|
32
|
-
const pid = parseInt(await readFile(pidFile, 'utf-8'), 10);
|
|
33
|
-
process.kill(pid, 'SIGTERM');
|
|
34
|
-
await unlink(pidFile);
|
|
35
|
-
} catch {
|
|
36
|
-
// No daemon running or already stopped
|
|
37
|
-
}
|
|
38
|
-
} catch {
|
|
39
|
-
// Silent fail — cleanup is best-effort
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
main();
|
package/src/index.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import { initCommand } from './commands/init.js';
|
|
5
|
-
import { loginCommand } from './commands/login.js';
|
|
6
|
-
import { lockCommand } from './commands/lock.js';
|
|
7
|
-
import { unlockCommand } from './commands/unlock.js';
|
|
8
|
-
import { statusCommand } from './commands/status.js';
|
|
9
|
-
import { checkCommand } from './commands/check.js';
|
|
10
|
-
import { whoamiCommand } from './commands/whoami.js';
|
|
11
|
-
import { configCommand } from './commands/config.js';
|
|
12
|
-
import { installHooksCommand } from './commands/install-hooks.js';
|
|
13
|
-
|
|
14
|
-
const program = new Command();
|
|
15
|
-
|
|
16
|
-
program
|
|
17
|
-
.name('preclaim')
|
|
18
|
-
.description('AI File Coordination Layer — predictive file locking for AI coding agents')
|
|
19
|
-
.version('0.1.0');
|
|
20
|
-
|
|
21
|
-
program
|
|
22
|
-
.command('init')
|
|
23
|
-
.description('Initialize Preclaim in the current project')
|
|
24
|
-
.option('--backend <url>', 'Backend URL', 'https://preclaim.vercel.app')
|
|
25
|
-
.option('--project-id <id>', 'Project ID')
|
|
26
|
-
.action(initCommand);
|
|
27
|
-
|
|
28
|
-
program
|
|
29
|
-
.command('login')
|
|
30
|
-
.description('Authenticate with Preclaim')
|
|
31
|
-
.action(loginCommand);
|
|
32
|
-
|
|
33
|
-
program
|
|
34
|
-
.command('lock <file>')
|
|
35
|
-
.description('Lock a file')
|
|
36
|
-
.option('-s, --session <id>', 'Session ID')
|
|
37
|
-
.option('-t, --ttl <minutes>', 'Lock TTL in minutes')
|
|
38
|
-
.action(lockCommand);
|
|
39
|
-
|
|
40
|
-
program
|
|
41
|
-
.command('unlock [file]')
|
|
42
|
-
.description('Release a file lock')
|
|
43
|
-
.option('-s, --session <id>', 'Session ID')
|
|
44
|
-
.option('-a, --all', 'Release all locks for this session')
|
|
45
|
-
.action(unlockCommand);
|
|
46
|
-
|
|
47
|
-
program
|
|
48
|
-
.command('status')
|
|
49
|
-
.description('Show active locks for this project')
|
|
50
|
-
.action(statusCommand);
|
|
51
|
-
|
|
52
|
-
program
|
|
53
|
-
.command('check <files...>')
|
|
54
|
-
.description('Check lock status for files')
|
|
55
|
-
.action(checkCommand);
|
|
56
|
-
|
|
57
|
-
program
|
|
58
|
-
.command('whoami')
|
|
59
|
-
.description('Show current user info')
|
|
60
|
-
.action(whoamiCommand);
|
|
61
|
-
|
|
62
|
-
program
|
|
63
|
-
.command('config')
|
|
64
|
-
.description('View or modify project configuration')
|
|
65
|
-
.option('--get <key>', 'Get a config value')
|
|
66
|
-
.option('--set <key=value>', 'Set a config value')
|
|
67
|
-
.action(configCommand);
|
|
68
|
-
|
|
69
|
-
program
|
|
70
|
-
.command('install-hooks')
|
|
71
|
-
.description('Install Claude Code hooks in the current project')
|
|
72
|
-
.action(installHooksCommand);
|
|
73
|
-
|
|
74
|
-
program.parse();
|
package/src/lib/auth.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { loadCredentials, type PreclaimCredentials } from '@preclaim/core';
|
|
2
|
-
|
|
3
|
-
export async function requireAuth(): Promise<PreclaimCredentials> {
|
|
4
|
-
const creds = await loadCredentials();
|
|
5
|
-
if (!creds) {
|
|
6
|
-
console.error('Not logged in. Run `preclaim login` first.');
|
|
7
|
-
process.exit(1);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// Check expiry
|
|
11
|
-
if (new Date(creds.expiresAt) < new Date()) {
|
|
12
|
-
console.error('Session expired. Run `preclaim login` to re-authenticate.');
|
|
13
|
-
process.exit(1);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return creds;
|
|
17
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { PreclaimClient, findConfig, type PreclaimConfig, type PreclaimCredentials } from '@preclaim/core';
|
|
2
|
-
import { requireAuth } from './auth.js';
|
|
3
|
-
|
|
4
|
-
export interface ResolvedContext {
|
|
5
|
-
client: PreclaimClient;
|
|
6
|
-
config: PreclaimConfig;
|
|
7
|
-
credentials: PreclaimCredentials;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function resolveContext(): Promise<ResolvedContext> {
|
|
11
|
-
const credentials = await requireAuth();
|
|
12
|
-
|
|
13
|
-
const found = await findConfig();
|
|
14
|
-
if (!found) {
|
|
15
|
-
console.error('No .preclaim.json found. Run `preclaim init` in your project root.');
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const client = new PreclaimClient({
|
|
20
|
-
baseUrl: found.config.backend,
|
|
21
|
-
accessToken: credentials.accessToken,
|
|
22
|
-
timeoutMs: 5000,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
return { client, config: found.config, credentials };
|
|
26
|
-
}
|
package/src/lib/hook-io.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// Hook I/O helpers for Claude Code hooks
|
|
2
|
-
// Reads hook input from stdin and writes hook output to stdout
|
|
3
|
-
|
|
4
|
-
export interface ClaudeHookInput {
|
|
5
|
-
session_id: string;
|
|
6
|
-
tool_name?: string;
|
|
7
|
-
tool_input?: Record<string, unknown>;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ClaudeHookOutput {
|
|
11
|
-
permissionDecision?: 'allow' | 'deny';
|
|
12
|
-
reason?: string;
|
|
13
|
-
systemMessage?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function readHookInput(): Promise<ClaudeHookInput> {
|
|
17
|
-
return new Promise((resolve, reject) => {
|
|
18
|
-
let data = '';
|
|
19
|
-
process.stdin.setEncoding('utf-8');
|
|
20
|
-
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
21
|
-
process.stdin.on('end', () => {
|
|
22
|
-
try {
|
|
23
|
-
resolve(JSON.parse(data));
|
|
24
|
-
} catch {
|
|
25
|
-
reject(new Error('Failed to parse hook input'));
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
process.stdin.on('error', reject);
|
|
29
|
-
|
|
30
|
-
// Timeout after 3s
|
|
31
|
-
setTimeout(() => reject(new Error('Hook input timeout')), 3000);
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function writeHookOutput(output: ClaudeHookOutput): void {
|
|
36
|
-
process.stdout.write(JSON.stringify(output));
|
|
37
|
-
}
|
package/src/lib/output.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { Lock } from '@preclaim/core';
|
|
2
|
-
|
|
3
|
-
export function formatLock(lock: Lock): string {
|
|
4
|
-
const acquired = new Date(lock.acquired_at).toLocaleTimeString();
|
|
5
|
-
const expires = new Date(lock.expires_at).toLocaleTimeString();
|
|
6
|
-
return ` ${lock.file_path} (session: ${lock.session_id.slice(0, 8)}… acquired: ${acquired} expires: ${expires})`;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function formatLockTable(locks: Lock[]): string {
|
|
10
|
-
if (locks.length === 0) {
|
|
11
|
-
return 'No active locks.';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const lines = ['Active locks:', ''];
|
|
15
|
-
for (const lock of locks) {
|
|
16
|
-
lines.push(formatLock(lock));
|
|
17
|
-
}
|
|
18
|
-
lines.push('', `Total: ${locks.length} lock(s)`);
|
|
19
|
-
return lines.join('\n');
|
|
20
|
-
}
|