Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Documentation.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# GitAgent Documentation

> **GitAgent** — A universal git-native multimodal always-learning AI Agent
> Version 1.3.3 | MIT License | [github.com/open-gitagent/gitagent](https://github.com/open-gitagent/gitagent)
> Version 1.5.2 | MIT License | [github.com/open-gitagent/gitagent](https://github.com/open-gitagent/gitagent)

---

Expand Down Expand Up @@ -83,7 +83,7 @@ The installer offers four options:
curl -fsSL https://raw.githubusercontent.com/open-gitagent/gitagent/main/install.sh | bash

# Or manually
npm update -g gitagent
npm update -g @open-gitagent/gitagent
```

---
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
</p>

<p align="center">
<img src="https://img.shields.io/npm/v/gitagent?style=flat-square&color=blue" alt="npm version" />
<img src="https://img.shields.io/npm/v/@open-gitagent/gitagent?style=flat-square&color=blue" alt="npm version" />
<img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen?style=flat-square" alt="node version" />
<img src="https://img.shields.io/github/license/open-gitagent/gitagent?style=flat-square" alt="license" />
<img src="https://img.shields.io/badge/TypeScript-5.7-blue?style=flat-square&logo=typescript&logoColor=white" alt="typescript" />
Expand Down Expand Up @@ -56,7 +56,7 @@ This will:
- Walk you through API key setup (Quick or Advanced mode)
- Launch the voice UI in your browser at `http://localhost:3333`

> **Requirements:** Node.js 18+, npm, git
> **Requirements:** Node.js 20+, npm, git

### Or install manually:

Expand Down Expand Up @@ -780,7 +780,7 @@ Your agent lives in a git repository with structured files:
### Installation & Setup

**What are the requirements?**
Node.js 18+ (or 20+ recommended), npm, and git. Install globally with `npm install -g @open-gitagent/gitagent` (slim CLI + SDK). Add `@open-gitagent/voice` for voice mode + the web UI.
Node.js 20+, npm, and git. Install globally with `npm install -g @open-gitagent/gitagent` (slim CLI + SDK). Add `@open-gitagent/voice` for voice mode + the web UI.

**How do I set up API keys?**
Run the installer for guided setup:
Expand Down
4 changes: 2 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ check_cmd npm
check_cmd git

NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
echo -e " ${RED}✗ Node.js 18+ required (found $(node -v))${NC}"
if [ "$NODE_VERSION" -lt 20 ]; then
echo -e " ${RED}✗ Node.js 20+ required (found $(node -v))${NC}"
exit 1
fi

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"build": "tsc",
"dev": "tsc --watch",
"start": "node dist/index.js",
"test": "node --test test/*.test.ts --experimental-strip-types"
"test": "npx tsx --test test/*.test.ts"
},
"engines": {
"node": ">=20"
Expand Down
6 changes: 0 additions & 6 deletions src/__tests__/telemetry.test.ts

This file was deleted.

5 changes: 5 additions & 0 deletions src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ const _slots = {
export async function initTelemetry(opts: TelemetryOptions): Promise<void> {
if (_initialized) return;

// When there is no endpoint configured and no test provider, telemetry
// has nowhere to send data — skip the dynamic SDK imports entirely.
const hasEndpoint = opts.exporterEndpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
if (!opts._testProvider && !hasEndpoint) return;

try {
// Test path — register a caller-supplied TracerProvider directly.
if (opts._testProvider) {
Expand Down
7 changes: 0 additions & 7 deletions src/tools/__tests__/memory.test.ts

This file was deleted.

181 changes: 181 additions & 0 deletions test/memory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* Tests for the memory tool (src/tools/memory.ts).
*
* The memory tool provides git-backed persistent memory with load/save
* operations. Each save creates a git commit, giving full history of
* what the agent has remembered.
*/
import { describe, it, before } from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, rm, writeFile, mkdir } from "fs/promises";
import { join } from "path";
import { tmpdir } from "os";
import { execSync } from "child_process";

let createMemoryTool: typeof import("../src/tools/memory.ts").createMemoryTool;

before(async () => {
const mod = await import("../src/tools/memory.ts");
createMemoryTool = mod.createMemoryTool;
});

describe("memory tool", () => {
async function setupRepo(): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), "gitagent-memory-test-"));
execSync("git init -q", { cwd: dir });
execSync('git config --local user.email "test@gitagent.test"', { cwd: dir });
execSync('git config --local user.name "Test Agent"', { cwd: dir });
return dir;
}

async function cleanup(dir: string): Promise<void> {
await rm(dir, { recursive: true, force: true }).catch(() => {});
}

describe("load", () => {
it("returns stored memory content", async () => {
const dir = await setupRepo();
try {
const tool = createMemoryTool(dir);

await tool.execute("call-1", {
action: "save",
content: "# Memory\n\n- Remember to buy milk\n- Project uses TypeScript",
message: "Initial memory",
});

const result = await tool.execute("call-2", { action: "load" });

assert.ok(result.content);
assert.equal(result.content.length, 1);
assert.ok(result.content[0].text.includes("Remember to buy milk"));
assert.ok(result.content[0].text.includes("Project uses TypeScript"));
} finally {
await cleanup(dir);
}
});

it("returns 'No memories yet.' when memory file is empty or missing", async () => {
const dir = await setupRepo();
try {
const tool = createMemoryTool(dir);

const result = await tool.execute("call-1", { action: "load" });

assert.equal(result.content[0].text, "No memories yet.");
} finally {
await cleanup(dir);
}
});

it("returns 'No memories yet.' when memory file has only heading", async () => {
const dir = await setupRepo();
try {
await mkdir(join(dir, "memory"), { recursive: true });
await writeFile(join(dir, "memory", "MEMORY.md"), "# Memory", "utf-8");

const tool = createMemoryTool(dir);
const result = await tool.execute("call-1", { action: "load" });

assert.equal(result.content[0].text, "No memories yet.");
} finally {
await cleanup(dir);
}
});
});

describe("save", () => {
it("writes content and commits to git", async () => {
const dir = await setupRepo();
try {
const tool = createMemoryTool(dir);

const result = await tool.execute("call-1", {
action: "save",
content: "# Memory\n\nSaved entry one.",
message: "First save",
});

assert.equal(result.content.length, 1);
assert.ok(
result.content[0].text.includes("Memory saved and committed"),
);
assert.ok(result.content[0].text.includes("First save"));

const { readFile } = await import("fs/promises");
const fileContent = await readFile(
join(dir, "memory", "MEMORY.md"),
"utf-8",
);
assert.ok(fileContent.includes("Saved entry one"));

const log = execSync("git log --oneline", {
cwd: dir,
encoding: "utf-8",
});
assert.ok(log.includes("First save"), `git log should contain commit: ${log}`);
} finally {
await cleanup(dir);
}
});

it("uses default commit message when message is omitted", async () => {
const dir = await setupRepo();
try {
const tool = createMemoryTool(dir);

await tool.execute("call-1", {
action: "save",
content: "Memory without explicit message.",
});

const log = execSync("git log --oneline", {
cwd: dir,
encoding: "utf-8",
});
assert.ok(
log.includes("Update memory"),
`commit should default to "Update memory": ${log}`,
);
} finally {
await cleanup(dir);
}
});

it("requires content for save action", async () => {
const dir = await setupRepo();
try {
const tool = createMemoryTool(dir);

await assert.rejects(
() =>
tool.execute("call-1", {
action: "save",
}),
/content is required for save action/,
);
} finally {
await cleanup(dir);
}
});
});

describe("abort signal", () => {
it("throws when signal is already aborted", async () => {
const dir = await setupRepo();
try {
const tool = createMemoryTool(dir);
const controller = new AbortController();
controller.abort();

await assert.rejects(
() =>
tool.execute("call-1", { action: "load" }, controller.signal),
/Operation aborted/,
);
} finally {
await cleanup(dir);
}
});
});
});
121 changes: 121 additions & 0 deletions test/telemetry-init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Tests for the telemetry module (src/telemetry.ts) — init/shutdown/idempotency.
*
* These tests verify that initTelemetry correctly gates on the OTLP
* endpoint environment variable: it MUST return without enabling telemetry
* when no endpoint is configured, and it MUST successfully create an SDK
* instance when an endpoint (or test provider) is provided.
*/
import { describe, it, before, afterEach } from "node:test";
import assert from "node:assert/strict";
import { trace } from "@opentelemetry/api";
import {
NodeTracerProvider,
InMemorySpanExporter,
SimpleSpanProcessor,
} from "@opentelemetry/sdk-trace-node";

let initTelemetry: typeof import("../src/telemetry.ts").initTelemetry;
let shutdownTelemetry: typeof import("../src/telemetry.ts").shutdownTelemetry;
let isTelemetryEnabled: typeof import("../src/telemetry.ts").isTelemetryEnabled;

before(async () => {
const mod = await import("../src/telemetry.ts");
initTelemetry = mod.initTelemetry;
shutdownTelemetry = mod.shutdownTelemetry;
isTelemetryEnabled = mod.isTelemetryEnabled;
});

afterEach(async () => {
await shutdownTelemetry();
try {
trace.disable();
} catch {
/* ignore */
}
});

describe("telemetry init", () => {
function makeTestProvider() {
const exporter = new InMemorySpanExporter();
const provider = new NodeTracerProvider({
spanProcessors: [new SimpleSpanProcessor(exporter)],
});
return { exporter, provider };
}

it("returns without enabling telemetry when no OTLP endpoint is configured", async () => {
const saved = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
const wasSet = "OTEL_EXPORTER_OTLP_ENDPOINT" in process.env;
delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;

try {
await assert.doesNotReject(
() => initTelemetry({}),
"initTelemetry must never throw, even without an endpoint",
);

assert.equal(
isTelemetryEnabled(),
false,
"telemetry must remain disabled when no endpoint is configured",
);
} finally {
if (wasSet) {
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = saved;
} else {
delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
}
}
});

it("creates an SDK instance when endpoint is configured", async () => {
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318";
const { exporter, provider } = makeTestProvider();

try {
await initTelemetry({
serviceName: "test-svc",
_testProvider: provider,
});

assert.equal(
isTelemetryEnabled(),
true,
"telemetry must be enabled after initTelemetry with _testProvider",
);

const tracer = trace.getTracer("test");
const span = tracer.startSpan("test-span");
span.end();

await provider.forceFlush();
const spans = exporter.getFinishedSpans();
assert.equal(spans.length, 1, "span should be exported");
assert.equal(spans[0].name, "test-span");
} finally {
delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
}
});

it("is idempotent", async () => {
const { provider: provider1 } = makeTestProvider();
const { provider: provider2 } = makeTestProvider();

await initTelemetry({ _testProvider: provider1 });
assert.equal(isTelemetryEnabled(), true);

await initTelemetry({ _testProvider: provider2 });
assert.equal(isTelemetryEnabled(), true);
});

it("shutdownTelemetry resets the initialized state", async () => {
const { provider } = makeTestProvider();

await initTelemetry({ _testProvider: provider });
assert.equal(isTelemetryEnabled(), true);

await shutdownTelemetry();
assert.equal(isTelemetryEnabled(), false);
});
});