diff --git a/coverage.txt b/coverage.txt index c7022a12..5d53ebd6 100644 --- a/coverage.txt +++ b/coverage.txt @@ -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 | @@ -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 diff --git a/openspec/changes/add-date-tool/tasks.md b/openspec/changes/add-date-tool/tasks.md deleted file mode 100644 index 22e98d78..00000000 --- a/openspec/changes/add-date-tool/tasks.md +++ /dev/null @@ -1,23 +0,0 @@ -## 1. Create date tool implementation - -- [ ] 1.1 Create `src/tools/date.js` with `dateImpl` function that uses a ternary: `format === "human" ? new Date().toString() : new Date().toISOString()` -- [ ] 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 - -- [ ] 2.1 Import `createDateTool` in `src/tools/index.js` -- [ ] 2.2 Add `date: []` to `TOOL_PERMISSIONS` (zero required permissions) -- [ ] 2.3 Add `date: createDateTool` to `TOOL_FACTORIES` -- [ ] 2.4 Add `case "date":` to the switch or default handler in `buildToolConfig` - -## 3. Write tests - -- [ ] 3.1 Create `tests/unit/tools_date.test.js` with tests for ISO 8601 format (default and explicit) -- [ ] 3.2 Test for human-readable format output -- [ ] 3.3 Test that `createDateTool` returns a LangChain Tool with correct name, description, and schema -- [ ] 3.4 Test that `date` tool registers without permissions in `buildToolConfig` - -## 4. Verify - -- [ ] 4.1 Run `npm run lint` to confirm no lint errors -- [ ] 4.2 Run `npm run test` to confirm all tests pass diff --git a/openspec/changes/add-date-tool/.openspec.yaml b/openspec/changes/archive/2026-06-03-add-date-tool/.openspec.yaml similarity index 100% rename from openspec/changes/add-date-tool/.openspec.yaml rename to openspec/changes/archive/2026-06-03-add-date-tool/.openspec.yaml diff --git a/openspec/changes/add-date-tool/design.md b/openspec/changes/archive/2026-06-03-add-date-tool/design.md similarity index 100% rename from openspec/changes/add-date-tool/design.md rename to openspec/changes/archive/2026-06-03-add-date-tool/design.md diff --git a/openspec/changes/add-date-tool/proposal.md b/openspec/changes/archive/2026-06-03-add-date-tool/proposal.md similarity index 100% rename from openspec/changes/add-date-tool/proposal.md rename to openspec/changes/archive/2026-06-03-add-date-tool/proposal.md diff --git a/openspec/changes/add-date-tool/specs/date-tool/spec.md b/openspec/changes/archive/2026-06-03-add-date-tool/specs/date-tool/spec.md similarity index 100% rename from openspec/changes/add-date-tool/specs/date-tool/spec.md rename to openspec/changes/archive/2026-06-03-add-date-tool/specs/date-tool/spec.md diff --git a/openspec/changes/archive/2026-06-03-add-date-tool/tasks.md b/openspec/changes/archive/2026-06-03-add-date-tool/tasks.md new file mode 100644 index 00000000..d82e7e1b --- /dev/null +++ b/openspec/changes/archive/2026-06-03-add-date-tool/tasks.md @@ -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 diff --git a/prompts/SYSTEM_PROMPT.md b/prompts/SYSTEM_PROMPT.md index 05d3d4b9..fbc46795 100644 --- a/prompts/SYSTEM_PROMPT.md +++ b/prompts/SYSTEM_PROMPT.md @@ -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. diff --git a/src/memory/context.js b/src/memory/context.js index b46dd4a0..f582e6bf 100644 --- a/src/memory/context.js +++ b/src/memory/context.js @@ -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); diff --git a/src/tools/date.js b/src/tools/date.js new file mode 100644 index 00000000..b50f7f96 --- /dev/null +++ b/src/tools/date.js @@ -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} 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} 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, + }); +} diff --git a/src/tools/index.js b/src/tools/index.js index d1488438..4b8bc8b0 100644 --- a/src/tools/index.js +++ b/src/tools/index.js @@ -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. @@ -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 @@ -72,6 +74,7 @@ const TOOL_FACTORIES = { text_to_speech: createTtsTool, mixture_of_agents: createMoaTool, sampling: createSamplingTool, + date: createDateTool, }; /** @@ -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; } diff --git a/tests/unit/tool_index.test.js b/tests/unit/tool_index.test.js index 17fc1da3..e8c5fdcf 100644 --- a/tests/unit/tool_index.test.js +++ b/tests/unit/tool_index.test.js @@ -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 () => { @@ -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")); }); }); diff --git a/tests/unit/tools_date.test.js b/tests/unit/tools_date.test.js new file mode 100644 index 00000000..800b97b7 --- /dev/null +++ b/tests/unit/tools_date.test.js @@ -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(", ")}`, + ); + }); +});