codebot-ai 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/encryption.d.ts +67 -0
- package/dist/encryption.js +209 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +16 -2
- package/dist/metrics.d.ts +35 -1
- package/dist/metrics.js +130 -2
- package/dist/policy.d.ts +26 -0
- package/dist/policy.js +85 -4
- package/package.json +14 -4
- package/dist/games/tic-tac-toe.d.ts +0 -6
- package/dist/games/tic-tac-toe.js +0 -64
package/README.md
CHANGED
|
@@ -254,7 +254,7 @@ CodeBot v2.0.0 is built with security as a core architectural principle:
|
|
|
254
254
|
- **SSRF protection** — blocks localhost, private IPs, cloud metadata endpoints
|
|
255
255
|
- **Path safety** — blocks writes to system directories, detects path traversal
|
|
256
256
|
|
|
257
|
-
See [SECURITY.md](SECURITY.md)
|
|
257
|
+
See [SECURITY.md](SECURITY.md), [docs/HARDENING.md](docs/HARDENING.md), and [docs/SOC2_COMPLIANCE.md](docs/SOC2_COMPLIANCE.md) for the full security model and compliance readiness.
|
|
258
258
|
|
|
259
259
|
## Stability
|
|
260
260
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption at rest for CodeBot v2.1.0
|
|
3
|
+
*
|
|
4
|
+
* Provides AES-256-GCM encryption for audit logs, session files, and memory.
|
|
5
|
+
* Key derivation uses PBKDF2 from a user-supplied passphrase or machine identity.
|
|
6
|
+
*
|
|
7
|
+
* Encryption is opt-in: set CODEBOT_ENCRYPTION_KEY env var or
|
|
8
|
+
* configure encryption.passphrase in policy.json.
|
|
9
|
+
*
|
|
10
|
+
* NEVER throws — encryption failures fall back to plaintext with a warning.
|
|
11
|
+
*/
|
|
12
|
+
export interface EncryptionConfig {
|
|
13
|
+
/** Enable encryption at rest */
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
/** Passphrase for key derivation (or use CODEBOT_ENCRYPTION_KEY env var) */
|
|
16
|
+
passphrase?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Derives an AES-256 key from a passphrase using PBKDF2-SHA512.
|
|
20
|
+
* The salt is generated randomly and prepended to the output.
|
|
21
|
+
*/
|
|
22
|
+
export declare function deriveKey(passphrase: string, salt?: Buffer): {
|
|
23
|
+
key: Buffer;
|
|
24
|
+
salt: Buffer;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Get the encryption passphrase from environment or config.
|
|
28
|
+
* Returns null if encryption is not configured.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getPassphrase(config?: EncryptionConfig): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Check if encryption is enabled (passphrase available).
|
|
33
|
+
*/
|
|
34
|
+
export declare function isEncryptionEnabled(config?: EncryptionConfig): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Encrypt plaintext using AES-256-GCM.
|
|
37
|
+
*
|
|
38
|
+
* Output format: HEADER(4) + salt(32) + iv(16) + tag(16) + ciphertext(...)
|
|
39
|
+
* All binary, base64-encoded as a string for storage.
|
|
40
|
+
*
|
|
41
|
+
* Returns null on failure (never throws).
|
|
42
|
+
*/
|
|
43
|
+
export declare function encrypt(plaintext: string, passphrase: string): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Decrypt a base64-encoded ciphertext produced by encrypt().
|
|
46
|
+
* Returns null on failure (wrong key, tampered data, not encrypted).
|
|
47
|
+
*/
|
|
48
|
+
export declare function decrypt(encoded: string, passphrase: string): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Encrypt a JSONL line for file storage.
|
|
51
|
+
* If encryption is not configured, returns the original line unchanged.
|
|
52
|
+
*/
|
|
53
|
+
export declare function encryptLine(line: string, config?: EncryptionConfig): string;
|
|
54
|
+
/**
|
|
55
|
+
* Decrypt a JSONL line from file storage.
|
|
56
|
+
* Auto-detects encrypted vs plaintext lines.
|
|
57
|
+
*/
|
|
58
|
+
export declare function decryptLine(line: string, config?: EncryptionConfig): string;
|
|
59
|
+
/**
|
|
60
|
+
* Encrypt an entire file's content (for memory files which are Markdown, not JSONL).
|
|
61
|
+
*/
|
|
62
|
+
export declare function encryptContent(content: string, config?: EncryptionConfig): string;
|
|
63
|
+
/**
|
|
64
|
+
* Decrypt file content. Auto-detects encrypted vs plaintext.
|
|
65
|
+
*/
|
|
66
|
+
export declare function decryptContent(content: string, config?: EncryptionConfig): string;
|
|
67
|
+
//# sourceMappingURL=encryption.d.ts.map
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Encryption at rest for CodeBot v2.1.0
|
|
4
|
+
*
|
|
5
|
+
* Provides AES-256-GCM encryption for audit logs, session files, and memory.
|
|
6
|
+
* Key derivation uses PBKDF2 from a user-supplied passphrase or machine identity.
|
|
7
|
+
*
|
|
8
|
+
* Encryption is opt-in: set CODEBOT_ENCRYPTION_KEY env var or
|
|
9
|
+
* configure encryption.passphrase in policy.json.
|
|
10
|
+
*
|
|
11
|
+
* NEVER throws — encryption failures fall back to plaintext with a warning.
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.deriveKey = deriveKey;
|
|
48
|
+
exports.getPassphrase = getPassphrase;
|
|
49
|
+
exports.isEncryptionEnabled = isEncryptionEnabled;
|
|
50
|
+
exports.encrypt = encrypt;
|
|
51
|
+
exports.decrypt = decrypt;
|
|
52
|
+
exports.encryptLine = encryptLine;
|
|
53
|
+
exports.decryptLine = decryptLine;
|
|
54
|
+
exports.encryptContent = encryptContent;
|
|
55
|
+
exports.decryptContent = decryptContent;
|
|
56
|
+
const crypto = __importStar(require("crypto"));
|
|
57
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
58
|
+
const IV_LENGTH = 16; // 128-bit IV for GCM
|
|
59
|
+
const TAG_LENGTH = 16; // 128-bit auth tag
|
|
60
|
+
const SALT_LENGTH = 32; // 256-bit salt for PBKDF2
|
|
61
|
+
const KEY_LENGTH = 32; // 256-bit key
|
|
62
|
+
const PBKDF2_ITERATIONS = 100_000;
|
|
63
|
+
const HEADER = 'CBE1'; // CodeBot Encrypted v1 — 4-byte magic header
|
|
64
|
+
/**
|
|
65
|
+
* Derives an AES-256 key from a passphrase using PBKDF2-SHA512.
|
|
66
|
+
* The salt is generated randomly and prepended to the output.
|
|
67
|
+
*/
|
|
68
|
+
function deriveKey(passphrase, salt) {
|
|
69
|
+
const s = salt || crypto.randomBytes(SALT_LENGTH);
|
|
70
|
+
const key = crypto.pbkdf2Sync(passphrase, s, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha512');
|
|
71
|
+
return { key, salt: s };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get the encryption passphrase from environment or config.
|
|
75
|
+
* Returns null if encryption is not configured.
|
|
76
|
+
*/
|
|
77
|
+
function getPassphrase(config) {
|
|
78
|
+
// Environment variable takes priority
|
|
79
|
+
const envKey = process.env.CODEBOT_ENCRYPTION_KEY;
|
|
80
|
+
if (envKey)
|
|
81
|
+
return envKey;
|
|
82
|
+
// Policy config passphrase
|
|
83
|
+
if (config?.passphrase)
|
|
84
|
+
return config.passphrase;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if encryption is enabled (passphrase available).
|
|
89
|
+
*/
|
|
90
|
+
function isEncryptionEnabled(config) {
|
|
91
|
+
if (config && !config.enabled)
|
|
92
|
+
return false;
|
|
93
|
+
return getPassphrase(config) !== null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Encrypt plaintext using AES-256-GCM.
|
|
97
|
+
*
|
|
98
|
+
* Output format: HEADER(4) + salt(32) + iv(16) + tag(16) + ciphertext(...)
|
|
99
|
+
* All binary, base64-encoded as a string for storage.
|
|
100
|
+
*
|
|
101
|
+
* Returns null on failure (never throws).
|
|
102
|
+
*/
|
|
103
|
+
function encrypt(plaintext, passphrase) {
|
|
104
|
+
try {
|
|
105
|
+
const { key, salt } = deriveKey(passphrase);
|
|
106
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
107
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
108
|
+
const encrypted = Buffer.concat([
|
|
109
|
+
cipher.update(plaintext, 'utf-8'),
|
|
110
|
+
cipher.final(),
|
|
111
|
+
]);
|
|
112
|
+
const tag = cipher.getAuthTag();
|
|
113
|
+
// Pack: header + salt + iv + tag + ciphertext
|
|
114
|
+
const packed = Buffer.concat([
|
|
115
|
+
Buffer.from(HEADER, 'ascii'),
|
|
116
|
+
salt,
|
|
117
|
+
iv,
|
|
118
|
+
tag,
|
|
119
|
+
encrypted,
|
|
120
|
+
]);
|
|
121
|
+
return packed.toString('base64');
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Decrypt a base64-encoded ciphertext produced by encrypt().
|
|
129
|
+
* Returns null on failure (wrong key, tampered data, not encrypted).
|
|
130
|
+
*/
|
|
131
|
+
function decrypt(encoded, passphrase) {
|
|
132
|
+
try {
|
|
133
|
+
const packed = Buffer.from(encoded, 'base64');
|
|
134
|
+
// Check magic header
|
|
135
|
+
const header = packed.subarray(0, 4).toString('ascii');
|
|
136
|
+
if (header !== HEADER)
|
|
137
|
+
return null; // Not encrypted data
|
|
138
|
+
// Unpack
|
|
139
|
+
let offset = 4;
|
|
140
|
+
const salt = packed.subarray(offset, offset + SALT_LENGTH);
|
|
141
|
+
offset += SALT_LENGTH;
|
|
142
|
+
const iv = packed.subarray(offset, offset + IV_LENGTH);
|
|
143
|
+
offset += IV_LENGTH;
|
|
144
|
+
const tag = packed.subarray(offset, offset + TAG_LENGTH);
|
|
145
|
+
offset += TAG_LENGTH;
|
|
146
|
+
const ciphertext = packed.subarray(offset);
|
|
147
|
+
// Derive same key from salt
|
|
148
|
+
const { key } = deriveKey(passphrase, salt);
|
|
149
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
150
|
+
decipher.setAuthTag(tag);
|
|
151
|
+
const decrypted = Buffer.concat([
|
|
152
|
+
decipher.update(ciphertext),
|
|
153
|
+
decipher.final(),
|
|
154
|
+
]);
|
|
155
|
+
return decrypted.toString('utf-8');
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Encrypt a JSONL line for file storage.
|
|
163
|
+
* If encryption is not configured, returns the original line unchanged.
|
|
164
|
+
*/
|
|
165
|
+
function encryptLine(line, config) {
|
|
166
|
+
const passphrase = getPassphrase(config);
|
|
167
|
+
if (!passphrase)
|
|
168
|
+
return line;
|
|
169
|
+
const encrypted = encrypt(line, passphrase);
|
|
170
|
+
if (!encrypted)
|
|
171
|
+
return line; // Fall back to plaintext
|
|
172
|
+
return encrypted;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Decrypt a JSONL line from file storage.
|
|
176
|
+
* Auto-detects encrypted vs plaintext lines.
|
|
177
|
+
*/
|
|
178
|
+
function decryptLine(line, config) {
|
|
179
|
+
// Quick check: if it doesn't look like base64 or doesn't start with our header when decoded, it's plaintext
|
|
180
|
+
if (line.startsWith('{') || line.startsWith('['))
|
|
181
|
+
return line;
|
|
182
|
+
const passphrase = getPassphrase(config);
|
|
183
|
+
if (!passphrase)
|
|
184
|
+
return line;
|
|
185
|
+
const decrypted = decrypt(line, passphrase);
|
|
186
|
+
return decrypted || line; // Fall back to original if decryption fails
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Encrypt an entire file's content (for memory files which are Markdown, not JSONL).
|
|
190
|
+
*/
|
|
191
|
+
function encryptContent(content, config) {
|
|
192
|
+
const passphrase = getPassphrase(config);
|
|
193
|
+
if (!passphrase)
|
|
194
|
+
return content;
|
|
195
|
+
return encrypt(content, passphrase) || content;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Decrypt file content. Auto-detects encrypted vs plaintext.
|
|
199
|
+
*/
|
|
200
|
+
function decryptContent(content, config) {
|
|
201
|
+
// If it starts with typical plaintext markers, it's not encrypted
|
|
202
|
+
if (content.startsWith('#') || content.startsWith('{') || content.startsWith('\n'))
|
|
203
|
+
return content;
|
|
204
|
+
const passphrase = getPassphrase(config);
|
|
205
|
+
if (!passphrase)
|
|
206
|
+
return content;
|
|
207
|
+
return decrypt(content, passphrase) || content;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=encryption.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "2.
|
|
1
|
+
export declare const VERSION = "2.1.0";
|
|
2
2
|
export { Agent } from './agent';
|
|
3
3
|
export { OpenAIProvider } from './providers/openai';
|
|
4
4
|
export { AnthropicProvider } from './providers/anthropic';
|
|
@@ -19,10 +19,14 @@ export type { IntegrityResult } from './integrity';
|
|
|
19
19
|
export { ReplayProvider, loadSessionForReplay, compareOutputs, listReplayableSessions } from './replay';
|
|
20
20
|
export type { SessionReplayData, ReplayDivergence } from './replay';
|
|
21
21
|
export { MetricsCollector } from './metrics';
|
|
22
|
-
export type { MetricsSnapshot, CounterValue, HistogramValue } from './metrics';
|
|
22
|
+
export type { MetricsSnapshot, CounterValue, HistogramValue, Span, SpanEvent } from './metrics';
|
|
23
23
|
export { RiskScorer } from './risk';
|
|
24
24
|
export type { RiskAssessment, RiskFactor } from './risk';
|
|
25
25
|
export { exportSarif, sarifToString } from './sarif';
|
|
26
26
|
export type { SarifLog, SarifResult, SarifRule } from './sarif';
|
|
27
|
+
export { PolicyEnforcer, loadPolicy, generateDefaultPolicyFile } from './policy';
|
|
28
|
+
export type { Policy, PolicyRbac, PolicyRole } from './policy';
|
|
29
|
+
export { encrypt, decrypt, encryptLine, decryptLine, encryptContent, decryptContent, isEncryptionEnabled, deriveKey, getPassphrase } from './encryption';
|
|
30
|
+
export type { EncryptionConfig } from './encryption';
|
|
27
31
|
export * from './types';
|
|
28
32
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.sarifToString = exports.exportSarif = exports.RiskScorer = exports.MetricsCollector = exports.listReplayableSessions = exports.compareOutputs = exports.loadSessionForReplay = exports.ReplayProvider = exports.verifyMessages = exports.verifyMessage = exports.signMessage = exports.deriveSessionKey = exports.CapabilityChecker = exports.detectProvider = exports.getModelInfo = exports.PROVIDER_DEFAULTS = exports.MODEL_REGISTRY = exports.loadMCPTools = exports.loadPlugins = exports.parseToolCalls = exports.MemoryManager = exports.SessionManager = exports.buildRepoMap = exports.ContextManager = exports.ToolRegistry = exports.AnthropicProvider = exports.OpenAIProvider = exports.Agent = exports.VERSION = void 0;
|
|
18
|
-
exports.VERSION = '2.
|
|
17
|
+
exports.getPassphrase = exports.deriveKey = exports.isEncryptionEnabled = exports.decryptContent = exports.encryptContent = exports.decryptLine = exports.encryptLine = exports.decrypt = exports.encrypt = exports.generateDefaultPolicyFile = exports.loadPolicy = exports.PolicyEnforcer = exports.sarifToString = exports.exportSarif = exports.RiskScorer = exports.MetricsCollector = exports.listReplayableSessions = exports.compareOutputs = exports.loadSessionForReplay = exports.ReplayProvider = exports.verifyMessages = exports.verifyMessage = exports.signMessage = exports.deriveSessionKey = exports.CapabilityChecker = exports.detectProvider = exports.getModelInfo = exports.PROVIDER_DEFAULTS = exports.MODEL_REGISTRY = exports.loadMCPTools = exports.loadPlugins = exports.parseToolCalls = exports.MemoryManager = exports.SessionManager = exports.buildRepoMap = exports.ContextManager = exports.ToolRegistry = exports.AnthropicProvider = exports.OpenAIProvider = exports.Agent = exports.VERSION = void 0;
|
|
18
|
+
exports.VERSION = '2.1.0';
|
|
19
19
|
var agent_1 = require("./agent");
|
|
20
20
|
Object.defineProperty(exports, "Agent", { enumerable: true, get: function () { return agent_1.Agent; } });
|
|
21
21
|
var openai_1 = require("./providers/openai");
|
|
@@ -62,5 +62,19 @@ Object.defineProperty(exports, "RiskScorer", { enumerable: true, get: function (
|
|
|
62
62
|
var sarif_1 = require("./sarif");
|
|
63
63
|
Object.defineProperty(exports, "exportSarif", { enumerable: true, get: function () { return sarif_1.exportSarif; } });
|
|
64
64
|
Object.defineProperty(exports, "sarifToString", { enumerable: true, get: function () { return sarif_1.sarifToString; } });
|
|
65
|
+
var policy_1 = require("./policy");
|
|
66
|
+
Object.defineProperty(exports, "PolicyEnforcer", { enumerable: true, get: function () { return policy_1.PolicyEnforcer; } });
|
|
67
|
+
Object.defineProperty(exports, "loadPolicy", { enumerable: true, get: function () { return policy_1.loadPolicy; } });
|
|
68
|
+
Object.defineProperty(exports, "generateDefaultPolicyFile", { enumerable: true, get: function () { return policy_1.generateDefaultPolicyFile; } });
|
|
69
|
+
var encryption_1 = require("./encryption");
|
|
70
|
+
Object.defineProperty(exports, "encrypt", { enumerable: true, get: function () { return encryption_1.encrypt; } });
|
|
71
|
+
Object.defineProperty(exports, "decrypt", { enumerable: true, get: function () { return encryption_1.decrypt; } });
|
|
72
|
+
Object.defineProperty(exports, "encryptLine", { enumerable: true, get: function () { return encryption_1.encryptLine; } });
|
|
73
|
+
Object.defineProperty(exports, "decryptLine", { enumerable: true, get: function () { return encryption_1.decryptLine; } });
|
|
74
|
+
Object.defineProperty(exports, "encryptContent", { enumerable: true, get: function () { return encryption_1.encryptContent; } });
|
|
75
|
+
Object.defineProperty(exports, "decryptContent", { enumerable: true, get: function () { return encryption_1.decryptContent; } });
|
|
76
|
+
Object.defineProperty(exports, "isEncryptionEnabled", { enumerable: true, get: function () { return encryption_1.isEncryptionEnabled; } });
|
|
77
|
+
Object.defineProperty(exports, "deriveKey", { enumerable: true, get: function () { return encryption_1.deriveKey; } });
|
|
78
|
+
Object.defineProperty(exports, "getPassphrase", { enumerable: true, get: function () { return encryption_1.getPassphrase; } });
|
|
65
79
|
__exportStar(require("./types"), exports);
|
|
66
80
|
//# sourceMappingURL=index.js.map
|
package/dist/metrics.d.ts
CHANGED
|
@@ -28,10 +28,32 @@ export interface MetricsSnapshot {
|
|
|
28
28
|
counters: CounterValue[];
|
|
29
29
|
histograms: HistogramValue[];
|
|
30
30
|
}
|
|
31
|
+
export interface SpanEvent {
|
|
32
|
+
name: string;
|
|
33
|
+
timestamp: number;
|
|
34
|
+
attributes?: Record<string, string | number | boolean>;
|
|
35
|
+
}
|
|
36
|
+
export interface Span {
|
|
37
|
+
traceId: string;
|
|
38
|
+
spanId: string;
|
|
39
|
+
parentSpanId?: string;
|
|
40
|
+
name: string;
|
|
41
|
+
kind: 'INTERNAL' | 'CLIENT' | 'SERVER';
|
|
42
|
+
startTimeUnixNano: number;
|
|
43
|
+
endTimeUnixNano: number;
|
|
44
|
+
attributes: Record<string, string | number | boolean>;
|
|
45
|
+
status: {
|
|
46
|
+
code: 0 | 1 | 2;
|
|
47
|
+
message?: string;
|
|
48
|
+
};
|
|
49
|
+
events: SpanEvent[];
|
|
50
|
+
}
|
|
31
51
|
export declare class MetricsCollector {
|
|
32
52
|
private sessionId;
|
|
33
53
|
private counters;
|
|
34
54
|
private histograms;
|
|
55
|
+
private traceId;
|
|
56
|
+
private spans;
|
|
35
57
|
constructor(sessionId?: string);
|
|
36
58
|
getSessionId(): string;
|
|
37
59
|
/** Increment a counter by delta (default 1) */
|
|
@@ -48,13 +70,25 @@ export declare class MetricsCollector {
|
|
|
48
70
|
save(sessionId?: string): void;
|
|
49
71
|
/** Human-readable per-tool breakdown */
|
|
50
72
|
formatSummary(): string;
|
|
73
|
+
/** Start a new span for tracking tool execution or other operations */
|
|
74
|
+
startSpan(name: string, attributes?: Record<string, string | number | boolean>, parentSpanId?: string): string;
|
|
75
|
+
/** End a span, optionally with error status */
|
|
76
|
+
endSpan(spanId: string, error?: string): void;
|
|
77
|
+
/** Add an event to an active span */
|
|
78
|
+
addSpanEvent(spanId: string, name: string, attributes?: Record<string, string | number | boolean>): void;
|
|
79
|
+
/** Get completed spans */
|
|
80
|
+
getSpans(): Span[];
|
|
81
|
+
/** Export traces to OTLP endpoint */
|
|
82
|
+
exportTraces(): void;
|
|
51
83
|
/**
|
|
52
84
|
* Export snapshot in OTLP JSON format via HTTP POST.
|
|
53
85
|
* Only fires when OTEL_EXPORTER_OTLP_ENDPOINT is set.
|
|
54
86
|
* Fails silently — never blocks or crashes.
|
|
55
87
|
*/
|
|
56
88
|
exportOtel(snap?: MetricsSnapshot): void;
|
|
57
|
-
/** Build OTLP-compatible
|
|
89
|
+
/** Build OTLP-compatible trace payload */
|
|
90
|
+
private buildOtlpTracePayload;
|
|
91
|
+
/** Build OTLP-compatible metrics JSON payload */
|
|
58
92
|
private buildOtlpPayload;
|
|
59
93
|
}
|
|
60
94
|
//# sourceMappingURL=metrics.d.ts.map
|
package/dist/metrics.js
CHANGED
|
@@ -49,6 +49,7 @@ const path = __importStar(require("path"));
|
|
|
49
49
|
const os = __importStar(require("os"));
|
|
50
50
|
const http = __importStar(require("http"));
|
|
51
51
|
const https = __importStar(require("https"));
|
|
52
|
+
const crypto = __importStar(require("crypto"));
|
|
52
53
|
// ── Helpers ──
|
|
53
54
|
/** Encode a metric key: name|label1=val1|label2=val2 (sorted labels) */
|
|
54
55
|
function encodeKey(name, labels) {
|
|
@@ -86,8 +87,11 @@ class MetricsCollector {
|
|
|
86
87
|
sessionId;
|
|
87
88
|
counters = new Map();
|
|
88
89
|
histograms = new Map();
|
|
90
|
+
traceId;
|
|
91
|
+
spans = [];
|
|
89
92
|
constructor(sessionId) {
|
|
90
93
|
this.sessionId = sessionId || `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
94
|
+
this.traceId = crypto.randomBytes(16).toString('hex');
|
|
91
95
|
}
|
|
92
96
|
getSessionId() {
|
|
93
97
|
return this.sessionId;
|
|
@@ -207,6 +211,85 @@ class MetricsCollector {
|
|
|
207
211
|
}
|
|
208
212
|
return lines.join('\n');
|
|
209
213
|
}
|
|
214
|
+
/** Start a new span for tracking tool execution or other operations */
|
|
215
|
+
startSpan(name, attributes, parentSpanId) {
|
|
216
|
+
const spanId = crypto.randomBytes(8).toString('hex');
|
|
217
|
+
const span = {
|
|
218
|
+
traceId: this.traceId,
|
|
219
|
+
spanId,
|
|
220
|
+
parentSpanId,
|
|
221
|
+
name,
|
|
222
|
+
kind: 'INTERNAL',
|
|
223
|
+
startTimeUnixNano: Date.now() * 1_000_000,
|
|
224
|
+
endTimeUnixNano: 0,
|
|
225
|
+
attributes: { 'session.id': this.sessionId, ...attributes },
|
|
226
|
+
status: { code: 0 },
|
|
227
|
+
events: [],
|
|
228
|
+
};
|
|
229
|
+
this.spans.push(span);
|
|
230
|
+
return spanId;
|
|
231
|
+
}
|
|
232
|
+
/** End a span, optionally with error status */
|
|
233
|
+
endSpan(spanId, error) {
|
|
234
|
+
const span = this.spans.find(s => s.spanId === spanId);
|
|
235
|
+
if (!span)
|
|
236
|
+
return;
|
|
237
|
+
span.endTimeUnixNano = Date.now() * 1_000_000;
|
|
238
|
+
if (error) {
|
|
239
|
+
span.status = { code: 2, message: error };
|
|
240
|
+
span.events.push({
|
|
241
|
+
name: 'exception',
|
|
242
|
+
timestamp: Date.now() * 1_000_000,
|
|
243
|
+
attributes: { 'exception.message': error },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
span.status = { code: 1 };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** Add an event to an active span */
|
|
251
|
+
addSpanEvent(spanId, name, attributes) {
|
|
252
|
+
const span = this.spans.find(s => s.spanId === spanId);
|
|
253
|
+
if (!span)
|
|
254
|
+
return;
|
|
255
|
+
span.events.push({ name, timestamp: Date.now() * 1_000_000, attributes });
|
|
256
|
+
}
|
|
257
|
+
/** Get completed spans */
|
|
258
|
+
getSpans() {
|
|
259
|
+
return this.spans.filter(s => s.endTimeUnixNano > 0);
|
|
260
|
+
}
|
|
261
|
+
/** Export traces to OTLP endpoint */
|
|
262
|
+
exportTraces() {
|
|
263
|
+
try {
|
|
264
|
+
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
265
|
+
if (!endpoint)
|
|
266
|
+
return;
|
|
267
|
+
const completedSpans = this.getSpans();
|
|
268
|
+
if (completedSpans.length === 0)
|
|
269
|
+
return;
|
|
270
|
+
const payload = this.buildOtlpTracePayload(completedSpans);
|
|
271
|
+
const url = new URL('/v1/traces', endpoint);
|
|
272
|
+
const body = JSON.stringify(payload);
|
|
273
|
+
const mod = url.protocol === 'https:' ? https : http;
|
|
274
|
+
const req = mod.request(url, {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
headers: {
|
|
277
|
+
'Content-Type': 'application/json',
|
|
278
|
+
'Content-Length': Buffer.byteLength(body),
|
|
279
|
+
},
|
|
280
|
+
timeout: 5000,
|
|
281
|
+
});
|
|
282
|
+
req.on('error', () => { });
|
|
283
|
+
req.on('timeout', () => req.destroy());
|
|
284
|
+
req.write(body);
|
|
285
|
+
req.end();
|
|
286
|
+
// Clear exported spans
|
|
287
|
+
this.spans = this.spans.filter(s => s.endTimeUnixNano === 0);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Trace export failures are non-fatal
|
|
291
|
+
}
|
|
292
|
+
}
|
|
210
293
|
/**
|
|
211
294
|
* Export snapshot in OTLP JSON format via HTTP POST.
|
|
212
295
|
* Only fires when OTEL_EXPORTER_OTLP_ENDPOINT is set.
|
|
@@ -239,7 +322,52 @@ class MetricsCollector {
|
|
|
239
322
|
// OTLP export failures are non-fatal
|
|
240
323
|
}
|
|
241
324
|
}
|
|
242
|
-
/** Build OTLP-compatible
|
|
325
|
+
/** Build OTLP-compatible trace payload */
|
|
326
|
+
buildOtlpTracePayload(spans) {
|
|
327
|
+
return {
|
|
328
|
+
resourceSpans: [{
|
|
329
|
+
resource: {
|
|
330
|
+
attributes: [
|
|
331
|
+
{ key: 'service.name', value: { stringValue: 'codebot' } },
|
|
332
|
+
{ key: 'service.version', value: { stringValue: '2.1.0' } },
|
|
333
|
+
{ key: 'session.id', value: { stringValue: this.sessionId } },
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
scopeSpans: [{
|
|
337
|
+
scope: { name: 'codebot-agent', version: '2.1.0' },
|
|
338
|
+
spans: spans.map(s => ({
|
|
339
|
+
traceId: s.traceId,
|
|
340
|
+
spanId: s.spanId,
|
|
341
|
+
parentSpanId: s.parentSpanId || '',
|
|
342
|
+
name: s.name,
|
|
343
|
+
kind: s.kind === 'CLIENT' ? 3 : s.kind === 'SERVER' ? 2 : 1,
|
|
344
|
+
startTimeUnixNano: String(s.startTimeUnixNano),
|
|
345
|
+
endTimeUnixNano: String(s.endTimeUnixNano),
|
|
346
|
+
attributes: Object.entries(s.attributes).map(([k, v]) => ({
|
|
347
|
+
key: k,
|
|
348
|
+
value: typeof v === 'number'
|
|
349
|
+
? { intValue: v }
|
|
350
|
+
: typeof v === 'boolean'
|
|
351
|
+
? { boolValue: v }
|
|
352
|
+
: { stringValue: String(v) },
|
|
353
|
+
})),
|
|
354
|
+
status: { code: s.status.code === 2 ? 2 : s.status.code === 1 ? 1 : 0, message: s.status.message || '' },
|
|
355
|
+
events: s.events.map(e => ({
|
|
356
|
+
name: e.name,
|
|
357
|
+
timeUnixNano: String(e.timestamp),
|
|
358
|
+
attributes: e.attributes
|
|
359
|
+
? Object.entries(e.attributes).map(([k, v]) => ({
|
|
360
|
+
key: k,
|
|
361
|
+
value: typeof v === 'number' ? { intValue: v } : { stringValue: String(v) },
|
|
362
|
+
}))
|
|
363
|
+
: [],
|
|
364
|
+
})),
|
|
365
|
+
})),
|
|
366
|
+
}],
|
|
367
|
+
}],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/** Build OTLP-compatible metrics JSON payload */
|
|
243
371
|
buildOtlpPayload(snap) {
|
|
244
372
|
const metrics = [];
|
|
245
373
|
for (const counter of snap.counters) {
|
|
@@ -285,7 +413,7 @@ class MetricsCollector {
|
|
|
285
413
|
],
|
|
286
414
|
},
|
|
287
415
|
scopeMetrics: [{
|
|
288
|
-
scope: { name: 'codebot-metrics', version: '1.
|
|
416
|
+
scope: { name: 'codebot-metrics', version: '2.1.0' },
|
|
289
417
|
metrics,
|
|
290
418
|
}],
|
|
291
419
|
}],
|
package/dist/policy.d.ts
CHANGED
|
@@ -48,6 +48,21 @@ export interface PolicyLimits {
|
|
|
48
48
|
max_files_per_operation?: number;
|
|
49
49
|
cost_limit_usd?: number;
|
|
50
50
|
}
|
|
51
|
+
export interface PolicyRole {
|
|
52
|
+
tools?: PolicyTools;
|
|
53
|
+
filesystem?: PolicyFilesystem;
|
|
54
|
+
limits?: PolicyLimits;
|
|
55
|
+
}
|
|
56
|
+
export interface PolicyRbac {
|
|
57
|
+
/** Map OS username or identifier to role name */
|
|
58
|
+
user_roles?: Record<string, string>;
|
|
59
|
+
/** Role definitions with scoped policies */
|
|
60
|
+
roles?: Record<string, PolicyRole>;
|
|
61
|
+
/** Default role for unrecognized users (falls back to base policy if not set) */
|
|
62
|
+
default_role?: string;
|
|
63
|
+
/** Enable RBAC enforcement (default: false for backward compatibility) */
|
|
64
|
+
enabled?: boolean;
|
|
65
|
+
}
|
|
51
66
|
export interface Policy {
|
|
52
67
|
version?: string;
|
|
53
68
|
execution?: PolicyExecution;
|
|
@@ -57,6 +72,7 @@ export interface Policy {
|
|
|
57
72
|
git?: PolicyGit;
|
|
58
73
|
mcp?: PolicyMcp;
|
|
59
74
|
limits?: PolicyLimits;
|
|
75
|
+
rbac?: PolicyRbac;
|
|
60
76
|
}
|
|
61
77
|
export declare const DEFAULT_POLICY: Required<Policy>;
|
|
62
78
|
/**
|
|
@@ -68,6 +84,16 @@ export declare class PolicyEnforcer {
|
|
|
68
84
|
private policy;
|
|
69
85
|
private projectRoot;
|
|
70
86
|
constructor(policy?: Policy, projectRoot?: string);
|
|
87
|
+
/** Resolve the current user's role and return merged policy */
|
|
88
|
+
private currentUser;
|
|
89
|
+
/** Set the current user for RBAC resolution */
|
|
90
|
+
setUser(username: string): void;
|
|
91
|
+
/** Get the current OS username */
|
|
92
|
+
getCurrentUser(): string;
|
|
93
|
+
/** Resolve the role for the current user */
|
|
94
|
+
resolveRole(): string | null;
|
|
95
|
+
/** Get the effective policy for the current user (base policy merged with role overrides) */
|
|
96
|
+
getEffectivePolicy(): Policy;
|
|
71
97
|
getPolicy(): Policy;
|
|
72
98
|
/** Check if a tool is enabled by policy. Returns { allowed, reason }. */
|
|
73
99
|
isToolAllowed(toolName: string): {
|
package/dist/policy.js
CHANGED
|
@@ -88,6 +88,12 @@ exports.DEFAULT_POLICY = {
|
|
|
88
88
|
max_files_per_operation: 20,
|
|
89
89
|
cost_limit_usd: 0, // 0 = no limit
|
|
90
90
|
},
|
|
91
|
+
rbac: {
|
|
92
|
+
enabled: false,
|
|
93
|
+
user_roles: {},
|
|
94
|
+
roles: {},
|
|
95
|
+
default_role: undefined,
|
|
96
|
+
},
|
|
91
97
|
};
|
|
92
98
|
// ── Policy Loader ──
|
|
93
99
|
/**
|
|
@@ -145,6 +151,8 @@ function validatePolicy(obj) {
|
|
|
145
151
|
return false;
|
|
146
152
|
if (p.limits !== undefined && typeof p.limits !== 'object')
|
|
147
153
|
return false;
|
|
154
|
+
if (p.rbac !== undefined && typeof p.rbac !== 'object')
|
|
155
|
+
return false;
|
|
148
156
|
return true;
|
|
149
157
|
}
|
|
150
158
|
/**
|
|
@@ -178,13 +186,60 @@ class PolicyEnforcer {
|
|
|
178
186
|
this.policy = policy || loadPolicy(projectRoot);
|
|
179
187
|
this.projectRoot = projectRoot || process.cwd();
|
|
180
188
|
}
|
|
189
|
+
/** Resolve the current user's role and return merged policy */
|
|
190
|
+
currentUser = null;
|
|
191
|
+
/** Set the current user for RBAC resolution */
|
|
192
|
+
setUser(username) {
|
|
193
|
+
this.currentUser = username;
|
|
194
|
+
}
|
|
195
|
+
/** Get the current OS username */
|
|
196
|
+
getCurrentUser() {
|
|
197
|
+
return this.currentUser || os.userInfo().username || 'unknown';
|
|
198
|
+
}
|
|
199
|
+
/** Resolve the role for the current user */
|
|
200
|
+
resolveRole() {
|
|
201
|
+
const rbac = this.policy.rbac;
|
|
202
|
+
if (!rbac?.enabled)
|
|
203
|
+
return null;
|
|
204
|
+
const user = this.getCurrentUser();
|
|
205
|
+
const roleName = rbac.user_roles?.[user] || rbac.default_role || null;
|
|
206
|
+
return roleName;
|
|
207
|
+
}
|
|
208
|
+
/** Get the effective policy for the current user (base policy merged with role overrides) */
|
|
209
|
+
getEffectivePolicy() {
|
|
210
|
+
const rbac = this.policy.rbac;
|
|
211
|
+
if (!rbac?.enabled)
|
|
212
|
+
return this.policy;
|
|
213
|
+
const roleName = this.resolveRole();
|
|
214
|
+
if (!roleName || !rbac.roles?.[roleName])
|
|
215
|
+
return this.policy;
|
|
216
|
+
const role = rbac.roles[roleName];
|
|
217
|
+
// Merge role overrides on top of base policy
|
|
218
|
+
const effective = { ...this.policy };
|
|
219
|
+
if (role.tools) {
|
|
220
|
+
effective.tools = {
|
|
221
|
+
...effective.tools,
|
|
222
|
+
...role.tools,
|
|
223
|
+
permissions: { ...effective.tools?.permissions, ...role.tools.permissions },
|
|
224
|
+
capabilities: { ...effective.tools?.capabilities, ...role.tools.capabilities },
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (role.filesystem) {
|
|
228
|
+
effective.filesystem = { ...effective.filesystem, ...role.filesystem };
|
|
229
|
+
}
|
|
230
|
+
if (role.limits) {
|
|
231
|
+
effective.limits = { ...effective.limits, ...role.limits };
|
|
232
|
+
}
|
|
233
|
+
return effective;
|
|
234
|
+
}
|
|
181
235
|
getPolicy() {
|
|
182
236
|
return this.policy;
|
|
183
237
|
}
|
|
184
238
|
// ── Tool Access ──
|
|
185
239
|
/** Check if a tool is enabled by policy. Returns { allowed, reason }. */
|
|
186
240
|
isToolAllowed(toolName) {
|
|
187
|
-
const
|
|
241
|
+
const effective = this.getEffectivePolicy();
|
|
242
|
+
const tools = effective.tools;
|
|
188
243
|
if (!tools)
|
|
189
244
|
return { allowed: true };
|
|
190
245
|
// If explicit disabled list contains it, block
|
|
@@ -203,7 +258,7 @@ class PolicyEnforcer {
|
|
|
203
258
|
}
|
|
204
259
|
/** Get the permission level for a tool (policy override or null for default). */
|
|
205
260
|
getToolPermission(toolName) {
|
|
206
|
-
return this.
|
|
261
|
+
return this.getEffectivePolicy().tools?.permissions?.[toolName] || null;
|
|
207
262
|
}
|
|
208
263
|
// ── Filesystem Access ──
|
|
209
264
|
/** Check if a path is writable according to policy. */
|
|
@@ -331,11 +386,11 @@ class PolicyEnforcer {
|
|
|
331
386
|
// ── Capabilities (v1.8.0) ──
|
|
332
387
|
/** Get capability restrictions for a tool. undefined = unrestricted. */
|
|
333
388
|
getToolCapabilities(toolName) {
|
|
334
|
-
return this.
|
|
389
|
+
return this.getEffectivePolicy().tools?.capabilities?.[toolName];
|
|
335
390
|
}
|
|
336
391
|
/** Check a specific capability for a tool. Returns { allowed, reason }. */
|
|
337
392
|
checkCapability(toolName, capabilityType, value) {
|
|
338
|
-
const caps = this.
|
|
393
|
+
const caps = this.getEffectivePolicy().tools?.capabilities;
|
|
339
394
|
if (!caps)
|
|
340
395
|
return { allowed: true };
|
|
341
396
|
const checker = new capabilities_1.CapabilityChecker(caps, this.projectRoot);
|
|
@@ -439,6 +494,32 @@ function generateDefaultPolicyFile() {
|
|
|
439
494
|
max_files_per_operation: 20,
|
|
440
495
|
cost_limit_usd: 0,
|
|
441
496
|
},
|
|
497
|
+
rbac: {
|
|
498
|
+
enabled: false,
|
|
499
|
+
default_role: 'developer',
|
|
500
|
+
user_roles: {},
|
|
501
|
+
roles: {
|
|
502
|
+
admin: {
|
|
503
|
+
tools: { disabled: [] },
|
|
504
|
+
limits: { max_iterations: 100, cost_limit_usd: 50.0 },
|
|
505
|
+
},
|
|
506
|
+
developer: {
|
|
507
|
+
tools: {
|
|
508
|
+
disabled: ['ssh_remote'],
|
|
509
|
+
permissions: { execute: 'prompt', write_file: 'prompt' },
|
|
510
|
+
},
|
|
511
|
+
limits: { max_iterations: 50, cost_limit_usd: 10.0 },
|
|
512
|
+
},
|
|
513
|
+
reviewer: {
|
|
514
|
+
tools: {
|
|
515
|
+
disabled: ['execute', 'write_file', 'edit_file', 'batch_edit', 'ssh_remote', 'docker', 'database'],
|
|
516
|
+
permissions: {},
|
|
517
|
+
},
|
|
518
|
+
filesystem: { writable_paths: [] },
|
|
519
|
+
limits: { max_iterations: 10, cost_limit_usd: 2.0 },
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
},
|
|
442
523
|
}, null, 2) + '\n';
|
|
443
524
|
}
|
|
444
525
|
//# sourceMappingURL=policy.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codebot-ai",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Zero-dependency autonomous AI agent. Code, browse, search, automate. Works with any LLM — Ollama, Claude, GPT, Gemini, DeepSeek, Groq, Mistral, Grok.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
"dev": "tsc --watch",
|
|
13
13
|
"start": "node dist/cli.js",
|
|
14
14
|
"test": "node --test dist/*.test.js dist/**/*.test.js",
|
|
15
|
-
"lint": "tsc --noEmit",
|
|
15
|
+
"lint": "eslint src/ && tsc --noEmit",
|
|
16
|
+
"lint:fix": "eslint src/ --fix",
|
|
17
|
+
"format": "prettier --write 'src/**/*.ts' '*.json' '*.md'",
|
|
18
|
+
"format:check": "prettier --check 'src/**/*.ts' '*.json' '*.md'",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
16
21
|
"prepublishOnly": "npm run build"
|
|
17
22
|
},
|
|
18
23
|
"keywords": [
|
|
@@ -71,7 +76,12 @@
|
|
|
71
76
|
"LICENSE"
|
|
72
77
|
],
|
|
73
78
|
"devDependencies": {
|
|
74
|
-
"
|
|
75
|
-
"@
|
|
79
|
+
"@types/node": "^20.0.0",
|
|
80
|
+
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
81
|
+
"@typescript-eslint/parser": "^8.56.1",
|
|
82
|
+
"eslint": "^10.0.2",
|
|
83
|
+
"eslint-config-prettier": "^10.1.8",
|
|
84
|
+
"prettier": "^3.8.1",
|
|
85
|
+
"typescript": "^5.5.0"
|
|
76
86
|
}
|
|
77
87
|
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// src/games/tic-tac-toe.ts
|
|
3
|
-
// Simple console-based Tic-Tac-Toe game
|
|
4
|
-
const gameBoard = Array(3).fill(null).map(() => Array(3).fill(null));
|
|
5
|
-
let currentPlayer = 'X';
|
|
6
|
-
const printBoard = () => {
|
|
7
|
-
for (const row of gameBoard) {
|
|
8
|
-
console.log(row.join(' | '));
|
|
9
|
-
}
|
|
10
|
-
};
|
|
11
|
-
const checkWin = () => {
|
|
12
|
-
// Check rows
|
|
13
|
-
for (let i = 0; i < 3; i++) {
|
|
14
|
-
if (gameBoard[i][0] === currentPlayer &&
|
|
15
|
-
gameBoard[i][1] === currentPlayer &&
|
|
16
|
-
gameBoard[i][2] === currentPlayer) {
|
|
17
|
-
console.log(`Player ${currentPlayer} wins!`);
|
|
18
|
-
process.exit(0);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
// Check columns
|
|
22
|
-
for (let j = 0; j < 3; j++) {
|
|
23
|
-
if (gameBoard[0][j] === currentPlayer &&
|
|
24
|
-
gameBoard[1][j] === currentPlayer &&
|
|
25
|
-
gameBoard[2][j] === currentPlayer) {
|
|
26
|
-
console.log(`Player ${currentPlayer} wins!`);
|
|
27
|
-
process.exit(0);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// Check diagonals
|
|
31
|
-
if ((gameBoard[0][0] === currentPlayer &&
|
|
32
|
-
gameBoard[1][1] === currentPlayer &&
|
|
33
|
-
gameBoard[2][2] === currentPlayer) ||
|
|
34
|
-
(gameBoard[0][2] === currentPlayer &&
|
|
35
|
-
gameBoard[1][1] === currentPlayer &&
|
|
36
|
-
gameBoard[2][0] === currentPlayer)) {
|
|
37
|
-
console.log(`Player ${currentPlayer} wins!`);
|
|
38
|
-
process.exit(0);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
const gameLoop = () => {
|
|
42
|
-
printBoard();
|
|
43
|
-
process.stdin.on('data', (input) => {
|
|
44
|
-
const [row, col] = input.toString().trim()
|
|
45
|
-
.split(',')
|
|
46
|
-
.map(Number);
|
|
47
|
-
if (row < 0 || row > 2 || col < 0 || col > 2) {
|
|
48
|
-
console.log('Invalid move. Try again.');
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (gameBoard[row][col]) {
|
|
52
|
-
console.log('Spot taken! Try again.');
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
gameBoard[row][col] = currentPlayer;
|
|
56
|
-
checkWin();
|
|
57
|
-
currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
|
|
58
|
-
process.stdin.removeAllListeners('data');
|
|
59
|
-
gameLoop();
|
|
60
|
-
});
|
|
61
|
-
};
|
|
62
|
-
// Start game
|
|
63
|
-
gameLoop();
|
|
64
|
-
//# sourceMappingURL=tic-tac-toe.js.map
|