klyro 0.1.7 → 0.1.8

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.
@@ -427,8 +427,15 @@ export async function run(opts, deps) {
427
427
  function redactOutput(v) {
428
428
  if (typeof v === 'string')
429
429
  return redact(v);
430
- if (v && typeof v === 'object')
431
- return v; // structured outputs are not redacted wholesale
430
+ if (Array.isArray(v))
431
+ return v.map((e) => redactOutput(e));
432
+ if (v && typeof v === 'object') {
433
+ const out = {};
434
+ for (const [k, val] of Object.entries(v)) {
435
+ out[k] = redactOutput(val);
436
+ }
437
+ return out;
438
+ }
432
439
  return v;
433
440
  }
434
441
  /**
@@ -57,7 +57,11 @@ export function withinBudget(system, messages, budget) {
57
57
  * Returns the new transcript plus a count of dropped observations.
58
58
  */
59
59
  export function compressTranscript(system, messages, budget) {
60
- const result = messages.slice();
60
+ // Deep copy to avoid mutating original messages (which are also stored in transcript/persistence)
61
+ const result = messages.map((m) => ({
62
+ role: m.role,
63
+ content: m.content.map((b) => ({ ...b, output: b.output })),
64
+ }));
61
65
  let dropped = 0;
62
66
  // Phase 1: find tool_call_ids referenced in any later assistant message.
63
67
  const consumed = new Set();
@@ -77,13 +81,14 @@ export function compressTranscript(system, messages, budget) {
77
81
  for (const b of m.content) {
78
82
  if (b.kind !== 'tool_result')
79
83
  continue;
80
- if (!consumed.has(b.toolCallId)) {
84
+ const block = b;
85
+ if (!consumed.has(block.toolCallId)) {
81
86
  // Drop the observation entirely.
82
87
  b.output = '[earlier observation removed to fit context]';
83
88
  dropped++;
84
89
  }
85
- else if (typeof b.output === 'string' && b.output.length > 400) {
86
- b.output = b.output.slice(0, 400) + '... [truncated]';
90
+ else if (typeof block.output === 'string' && block.output.length > 400) {
91
+ b.output = block.output.slice(0, 400) + '... [truncated]';
87
92
  }
88
93
  }
89
94
  }
@@ -32,7 +32,25 @@ export class SessionStore {
32
32
  }
33
33
  }
34
34
  async writeIndex(idx) {
35
- await fs.writeFile(this.indexPath, JSON.stringify(idx, null, 2));
35
+ const tmp = `${this.indexPath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
36
+ await fs.writeFile(tmp, JSON.stringify(idx, null, 2), 'utf-8');
37
+ try {
38
+ const fh = await fs.open(tmp, 'r+');
39
+ try {
40
+ await fh.sync();
41
+ }
42
+ finally {
43
+ await fh.close();
44
+ }
45
+ }
46
+ catch { /* ignore on Windows */ }
47
+ try {
48
+ await fs.rename(tmp, this.indexPath);
49
+ }
50
+ catch {
51
+ await fs.unlink(tmp).catch(() => undefined);
52
+ throw new Error('Failed to write sessions index');
53
+ }
36
54
  }
37
55
  async create(opts) {
38
56
  await this.ensureDir();
@@ -59,10 +77,40 @@ export class SessionStore {
59
77
  }
60
78
  async writeSession(id, data) {
61
79
  data.record.updatedAt = Date.now();
62
- await fs.writeFile(path.join(this.dir, `${id}.json`), JSON.stringify(data, null, 2));
63
- const idx = await this.readIndex();
64
- idx[id] = data.record;
65
- await this.writeIndex(idx);
80
+ const target = path.join(this.dir, `${id}.json`);
81
+ const tmp = `${target}.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
82
+ await fs.writeFile(tmp, JSON.stringify(data, null, 2), 'utf-8');
83
+ try {
84
+ const fh = await fs.open(tmp, 'r+');
85
+ try {
86
+ await fh.sync();
87
+ }
88
+ finally {
89
+ await fh.close();
90
+ }
91
+ }
92
+ catch { /* ignore */ }
93
+ try {
94
+ await fs.rename(tmp, target);
95
+ }
96
+ catch {
97
+ await fs.unlink(tmp).catch(() => undefined);
98
+ throw new Error(`Failed to write session ${id}`);
99
+ }
100
+ // Update index with simple retry for concurrent writers (optimistic)
101
+ for (let attempt = 0; attempt < 3; attempt++) {
102
+ try {
103
+ const idx = await this.readIndex();
104
+ idx[id] = data.record;
105
+ await this.writeIndex(idx);
106
+ return;
107
+ }
108
+ catch (err) {
109
+ if (attempt === 2)
110
+ throw err;
111
+ await new Promise((r) => setTimeout(r, 10 * (attempt + 1)));
112
+ }
113
+ }
66
114
  }
67
115
  async appendMessage(id, message) {
68
116
  const data = await this.readSession(id);
@@ -11,7 +11,10 @@
11
11
  import { Transform } from 'node:stream';
12
12
  const PATTERNS = [
13
13
  { name: 'aws-key', re: /AKIA[0-9A-Z]{16}/g },
14
- { name: 'aws-secret', re: /(?<![A-Za-z0-9])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9])/g },
14
+ // Specific: require secret context to avoid package-lock hash false positives
15
+ { name: 'aws-secret', re: /(?:aws_secret_access_key|secret)\s*[:=]\s*[A-Za-z0-9/+=]{40}/gi },
16
+ // High-entropy base64: require at least one +/= and not just hex (e.g. sha512 hex should not match)
17
+ { name: 'aws-secret-b64', re: /(?<![A-Za-z0-9/+=])(?=[A-Za-z0-9/+=]*[+/=])[A-Za-z0-9/+=]{40,}={0,2}(?![A-Za-z0-9/+=])/g, },
15
18
  { name: 'pem-block', re: /-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]*?-----END [A-Z ]+PRIVATE KEY-----/g },
16
19
  { name: 'github-token', re: /gh[pousr]_[A-Za-z0-9]{36,255}/g },
17
20
  { name: 'slack-token', re: /xox[abprs]-[A-Za-z0-9-]{10,}/g },
@@ -106,9 +106,13 @@ export const editFileTool = defineTool({
106
106
  throw Object.assign(new Error(`find substring occurs ${count} times in ${input.path}. Supply more context or pass replaceAll=true.`), { code: 'MATCH_AMBIGUOUS' });
107
107
  }
108
108
  let next = replaceAll ? original.split(findStr).join(input.replace) : original.replace(findStr, input.replace);
109
- // Preserve EOL
110
- if (eol === '\r\n')
111
- next = next.replace(/\n/g, '\r\n');
109
+ // Preserve EOL: normalize then convert
110
+ if (eol === '\r\n') {
111
+ next = next.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
112
+ }
113
+ else {
114
+ next = next.replace(/\r\n/g, '\n');
115
+ }
112
116
  // Preserve trailing newline
113
117
  if (hasTrailingNewline && !next.endsWith('\n'))
114
118
  next += eol;
@@ -64,6 +64,9 @@ const DANGEROUS_PATTERNS = [
64
64
  { pattern: /rm\s+-rf?\s+\.\s*($|[;&|])/, reason: 'recursive delete current directory' },
65
65
  { pattern: /rm\s+-rf?\s+\*\s*($|[;&|])/, reason: 'recursive delete all files via *' },
66
66
  { pattern: /rm\s+-rf?\s+\.\/\*\s*($|[;&|])/, reason: 'recursive delete all files' },
67
+ { pattern: /rm\s+-rf?\s+~(\/|$)/, reason: 'recursive delete home directory via ~' },
68
+ { pattern: /rm\s+-rf?\s+\$HOME\b/, reason: 'recursive delete home via $HOME' },
69
+ { pattern: /rm\s+-rf?\s+\$PWD\b/, reason: 'recursive delete via $PWD' },
67
70
  { pattern: /del\s+\/s\s+\/q\s+[a-z]:\\/i, reason: 'recursive delete on Windows drive root' },
68
71
  { pattern: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, reason: 'fork bomb' },
69
72
  { pattern: /bomb\(\)\s*\{\s*bomb\|bomb/, reason: 'fork bomb variant' },
@@ -71,7 +74,8 @@ const DANGEROUS_PATTERNS = [
71
74
  { pattern: /mkfs(\.|\s)/, reason: 'format filesystem' },
72
75
  { pattern: /dd\s+.*of=\/dev\//, reason: 'dd write to device' },
73
76
  { pattern: /chmod\s+-R\s+777\s+\//, reason: 'chmod 777 on root' },
74
- { pattern: /curl.*\|\s*(sh|bash|python|perl|ruby)/i, reason: 'curl|sh to unknown host' },
77
+ { pattern: /curl.*\|\s*(sh|bash|zsh|python|python3|perl|ruby|php)/i, reason: 'curl|sh to unknown host' },
78
+ { pattern: /wget.*\|\s*(sh|bash|python|perl|ruby)/i, reason: 'wget|sh pipe' },
75
79
  { pattern: /rm\s+-rf\s+--no-preserve-root\s+\//, reason: 'recursive delete --no-preserve-root' },
76
80
  ];
77
81
  export const shellExecTool = defineTool({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Klyro — autonomous coding harness CLI that streams from any OpenAI-compatible or Anthropic LLM endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",