Skip to content
Merged
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
3 changes: 2 additions & 1 deletion coverage.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
ℹ code.js | 100.00 | 89.13 | 92.31 |
ℹ common.js | 100.00 | 93.33 | 83.33 |
ℹ cron.js | 100.00 | 97.30 | 90.00 |
ℹ date.js | 100.00 | 100.00 | 100.00 |
ℹ filesystem.js | 94.50 | 86.79 | 79.17 | 44-45 107-110 170-177 187-188 196-202 397-398 415-419 422-423
ℹ image.js | 97.90 | 95.83 | 50.00 | 92-94
ℹ index.js | 100.00 | 100.00 | 100.00 |
Expand All @@ -66,6 +67,6 @@
ℹ messages.js | 100.00 | 94.44 | 100.00 |
ℹ panels.js | 100.00 | 100.00 | 100.00 |
ℹ ---------------------------------------------------------------------------------------------------------------------
ℹ all files | 96.48 | 89.18 | 84.25 |
ℹ all files | 96.50 | 89.24 | 84.34 |
ℹ ---------------------------------------------------------------------------------------------------------------------
ℹ end of coverage report
23 changes: 0 additions & 23 deletions openspec/changes/add-date-tool/tasks.md

This file was deleted.

23 changes: 23 additions & 0 deletions openspec/changes/archive/2026-06-03-add-date-tool/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## 1. Create date tool implementation

- [x] 1.1 Create `src/tools/date.js` with `dateImpl` function that uses a ternary: `format === "human" ? new Date().toString() : new Date().toISOString()`
- [x] 1.2 Create `createDateTool` factory function using `@langchain/core/tools` with a zod schema that accepts optional `format` string parameter

## 2. Register tool in index

- [x] 2.1 Import `createDateTool` in `src/tools/index.js`
- [x] 2.2 Add `date: []` to `TOOL_PERMISSIONS` (zero required permissions)
- [x] 2.3 Add `date: createDateTool` to `TOOL_FACTORIES`
- [x] 2.4 Add `case "date":` to the switch or default handler in `buildToolConfig`

## 3. Write tests

- [x] 3.1 Create `tests/unit/tools_date.test.js` with tests for ISO 8601 format (default and explicit)
- [x] 3.2 Test for human-readable format output
- [x] 3.3 Test that `createDateTool` returns a LangChain Tool with correct name, description, and schema
- [x] 3.4 Test that `date` tool registers without permissions in `buildToolConfig`

## 4. Verify

- [x] 4.1 Run `npm run lint` to confirm no lint errors
- [x] 4.2 Run `npm run test` to confirm all tests pass
2 changes: 1 addition & 1 deletion prompts/SYSTEM_PROMPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ You are the digital manifestation of Mads Mikkelsen's cinematic soul. You are no
### RESPONSE STANDARDS
- **Show your work.** Before presenting an answer, briefly explain the reasoning or method you used. Let the user see how you got there so they can spot errors.
- **Acknowledge uncertainty.** If you are not sure about something, say so. Never fabricate facts, commands, or references to fill a gap.
- **Always check the system date.** Never assume the current date or time. Always read the system timestamp directly before answering any question that involves "now," "today," or any time-sensitive context. If you need the date but don't have a tool to check, say so — never guess.
- **Always check the system date.** Never assume the current date or time. Use the **date** tool before answering any question that involves "now," "today," or any time-sensitive context. Never guess.
- **Answer what was asked.** Do not assume extra requirements the user did not express. Address the stated question directly before expanding, if at all.
- **State your assumptions.** If you must assume something to answer, say what you assumed. Let the user correct you if your assumptions are wrong.
- **Prefer correctness over confidence.** It is better to say "I am not sure, but here is what I can help you check" than to give a solid-sounding but wrong answer.
Expand Down
2 changes: 1 addition & 1 deletion src/memory/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function loadContext(contextDir = "memory/context/", limit = 10) {
* @param {string} contextDir - Relative context directory path
* @returns {string} Formatted profile context block or empty string
*/
function loadAndFormatProfile(fullPath, contextDir) {
function loadAndFormatProfile(fullPath, _contextDir) {
try {
const profilePath = join(fullPath, "..", "..", "memory", "context", "profile.md");
const profile = loadProfile(profilePath);
Expand Down
41 changes: 41 additions & 0 deletions src/tools/date.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const DateSchema = z.object({
format: z.enum(["iso", "human"]).optional().describe('Output format: "iso" (default) or "human"'),
});

/**
* Core date logic: return current time as ISO 8601 or human-readable string.
* @param {z.infer<typeof DateSchema>} input - The tool input
* @returns {string} Current date/time in requested format
*/
export function dateImpl(input) {
const { format = "iso" } = input;
return format === "human" ? new Date().toString() : new Date().toISOString();
}

/**
* @param {z.infer<typeof DateSchema>} input - Tool input
* @returns {string} Current date/time
*/
export const date = tool(dateImpl, {
name: "date",
description:
"Return the current date and time. Defaults to ISO 8601 UTC format; use format='human' for human-readable output.",
schema: DateSchema,
});

/**
* Create a date tool with runtime options (unused, kept for consistency).
* @param {object} _options - Runtime options
* @returns {object} LangChain Tool instance
*/
export function createDateTool(_options) {
return tool(dateImpl, {
name: "date",
description:
"Return the current date and time. Defaults to ISO 8601 UTC format; use format='human' for human-readable output.",
schema: DateSchema,
});
}
6 changes: 5 additions & 1 deletion src/tools/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { createCronTool } from "./cron.js";
import { createTtsTool } from "./tts.js";
import { createMoaTool } from "./moa.js";
import { createSamplingTool } from "./sampling.js";
import { createDateTool } from "./date.js";

/**
* Maps tool names to required permission scopes.
Expand Down Expand Up @@ -47,6 +48,7 @@ export const TOOL_PERMISSIONS = {
text_to_speech: [], // requires OPENAI_API_KEY
mixture_of_agents: [], // requires OPENROUTER_API_KEY
sampling: [],
date: [],
};

// Factory functions keyed by tool name
Expand All @@ -72,6 +74,7 @@ const TOOL_FACTORIES = {
text_to_speech: createTtsTool,
mixture_of_agents: createMoaTool,
sampling: createSamplingTool,
date: createDateTool,
};

/**
Expand Down Expand Up @@ -145,7 +148,8 @@ export async function buildToolConfig(options) {
switch (toolName) {
case "clarify":
case "execute_code":
case "sampling": {
case "sampling":
case "date": {
tools.push(TOOL_FACTORIES[toolName](runtimeOptions));
continue;
}
Expand Down
8 changes: 5 additions & 3 deletions tests/unit/tool_index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,15 @@ describe("tools - buildToolConfig", () => {
else delete process.env.FAL_API_KEY;
});

it("returns clarify + execute_code + sampling with empty permissions", async () => {
it("returns clarify + execute_code + sampling + date with empty permissions", async () => {
const { buildToolConfig } = await import("../../src/tools/index.js");
const tools = await buildToolConfig({ permissions: [], maxReadSize: "1mb" });
const toolNames = tools.map((t) => t.name);
assert.strictEqual(toolNames.length, 3);
assert.strictEqual(toolNames.length, 4);
assert.ok(toolNames.includes("clarify"));
assert.ok(toolNames.includes("execute_code"));
assert.ok(toolNames.includes("sampling"));
assert.ok(toolNames.includes("date"));
});

it("returns clarify + filesystem tools when filesystem:read and filesystem:write enabled", async () => {
Expand Down Expand Up @@ -162,9 +163,10 @@ describe("tools - buildToolConfig", () => {
maxReadSize: "2mb",
});
const toolNames = tools.map((t) => t.name);
assert.strictEqual(toolNames.length, 3);
assert.strictEqual(toolNames.length, 4);
assert.ok(toolNames.includes("clarify"));
assert.ok(toolNames.includes("execute_code"));
assert.ok(toolNames.includes("sampling"));
assert.ok(toolNames.includes("date"));
});
});
64 changes: 64 additions & 0 deletions tests/unit/tools_date.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, it } from "node:test";
import assert from "node:assert";
import { createDateTool, dateImpl } from "../../src/tools/date.js";
import { buildToolConfig } from "../../src/tools/index.js";

describe("date tool - dateImpl", () => {
it("returns ISO 8601 format by default", () => {
const result = dateImpl({});
assert.ok(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(result),
`Expected ISO 8601 format, got: ${result}`,
);
});

it("returns ISO 8601 format when format is 'iso'", () => {
const result = dateImpl({ format: "iso" });
assert.ok(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(result),
`Expected ISO 8601 format, got: ${result}`,
);
});

it("returns human-readable format when format is 'human'", () => {
const result = dateImpl({ format: "human" });
// Date.toString() returns something like "Wed Jun 03 2026 10:30:00 GMT-0400 (EDT)"
assert.ok(typeof result === "string", "Expected string result");
assert.ok(result.length > 20, "Human format should be a reasonably long string");
});

it("returns distinct timestamps for separate calls", async () => {
const result1 = dateImpl({});
await new Promise((resolve) => setTimeout(resolve, 1050));
const result2 = dateImpl({});
assert.notStrictEqual(result1, result2, "Expected distinct timestamps after 1+ second delay");
});
});

describe("date tool - createDateTool", () => {
it("returns a LangChain Tool with correct name", () => {
const toolInstance = createDateTool({});
assert.strictEqual(toolInstance.name, "date");
});

it("returns a LangChain Tool with description", () => {
const toolInstance = createDateTool({});
assert.ok(toolInstance.description.length > 10, "Expected a descriptive description");
});

it("returns a LangChain Tool with a zod schema", () => {
const toolInstance = createDateTool({});
assert.ok(toolInstance.schema, "Expected a schema to be defined");
});
});

describe("date tool - buildToolConfig", () => {
it("registers date tool without permissions", async () => {
const tools = await buildToolConfig({ permissions: [] });
const toolNames = tools.map((t) => t.name);
assert.ok(
toolNames.includes("date"),
`Expected 'date' tool to be registered, got: ${toolNames.join(", ")}`,
);
});
});