Strict ISO 8601 parsing, IANA timezone conversion, and UTC calendar arithmetic.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:omdsh-dev/dsh-tool-time
GitHub-sourced plugins run build scripts on your machine at install time. Only install sources you trust, and pin a commit (github:owner/repo#sha).
README
DSH time tool plugin — strict ISO parsing, IANA timezone conversion, UTC calendar arithmetic, fixed-duration diffs. Zero dependencies, zero processes, pure functions.
Motivation
Handling time information is one of the most frequent Agent needs — "what time is it now", "what date is 3 days from now", "convert UTC to Beijing time". Problems with the current bash date approach: a process is spawned every time, cross-platform syntax is inconsistent (macOS and GNU date -d are completely different), and timezone conversion relies on hand-stitching TZ=.... Mentally computing timezone offsets and leap-year/DST boundaries is a high-error area for models.
Security Model
No eval, no new Function. Input only passes through strict validation:
- Strict ISO 8601 subset: only accepts
YYYY-MM-DD(UTC midnight),YYYY-MM-DDTHH:mm:ssZ,YYYY-MM-DDTHH:mm:ss±HH:MM(within ±14:00, optional.SSSmilliseconds); datetimes without a timezone, RFC 2822, and natural-language dates are all rejected; calendar overflow such as2026-02-30is rejected by table-lookup validation (not relying on Date's silent normalization) - Timezone names: IANA names are validated via
Intl.DateTimeFormat; invalid ones throwtime: unknown timezone - Numbers:
amountmust be a safe integer;unitis an enumerated whitelist; all strings ≤ 200 characters - Fixed Intl environment:
'en-CA'+hourCycle: 'h23'(avoids midnight24:00and localized numerals)
Architecture
DSH Agent
│ ctx.tools.register()
▼
src/index.ts(Cordis 插件入口 + action 分发 + 独立校验)
│
▼
src/time.ts
├── parseStrictISO() — 严格 ISO 解析(正则 + 查表校验)
├── formatInTimezone() — Intl formatToParts 组装(时钟可注入)
├── addMonthsClamped() — 先置 1 日 → 目标年月 → 天数钳制
└── diffBetween() — 固定时长(sign + absolute 分解)
Tool Declaration
ctx.tools.register(defineTool({
name: 'time',
parameters: {
action: { type: 'string', required: true, enum: ['now', 'convert', 'add', 'diff'] },
value: { type: 'string', description: 'Strict ISO 8601 timestamp' },
timezone: { type: 'string', description: 'IANA timezone (default UTC; display only)' },
from: { type: 'string', description: 'diff start' },
to: { type: 'string', description: 'diff end' },
amount: { type: 'integer', description: 'Signed safe integer' },
unit: { type: 'string', enum: ['seconds','minutes','hours','days','weeks','months','years'] },
},
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v, null, 2) }] },
execute: (args) => Promise.resolve(executeAction(args.action, args) as JsonValue),
timeoutMs: 1000,
}))
Supported Operations
| action | Parameters | Returns |
|---|---|---|
now |
timezone? (default UTC) |
Structured representation of the current time |
convert |
value + timezone (required) |
Converted structured time |
add |
value + amount + unit (timezone? display only) |
Structured time after addition/subtraction |
diff |
from + to |
Fixed-duration diff (no months/years) |
Return structure (canonical value):
{
"iso": "2026-08-05T06:00:00.000Z",
"unix": 1785909600000,
"timezone": "Asia/Shanghai",
"timezoneSource": "explicit",
"local": "2026-08-05T14:00:00+08:00",
"formatted": "2026-08-05 14:00:00 (Asia/Shanghai)"
}
Semantic contract:
addalways operates in UTC (calendar-day semantics; keeps the "calendar day" intuition across DST);timezoneonly determines thelocal/formatteddisplay- Month/year clamping:
2026-01-31 + 1 month = 2026-02-28;2028-02-29 + 1 year = 2029-02-28(set to day 1 → locate the target year/month → clamp to the target month's max days) diffis fixed-duration only:sign+ signed total +absoluteremainder decomposition (months/years cannot be uniquely derived from milliseconds, so they are not provided). Field semantics (AUDIT-TIME-03):millisecondsis the exact signed total durationseconds/minutes/hours/days/weeksaresign * floor(abs / unit)(truncated on the absolute value, then signed)absoluteis a non-negative remainder decomposition ofabs(e.g. -1500ms →milliseconds: -1500, seconds: -1, absolute: {seconds: 1, milliseconds: 500})- A diff exceeding the safe integer range throws
time: diff duration out of range
- Default UTC: when
timezoneis omitted, the result is independent of the runtime environment (timezoneSource: "default-utc")
npm 0.1.0-rc.6 Compatibility (Verified)
This plugin has been migrated to the npm 0.1.0-rc.6 dependency line and fully verified end-to-end in an isolated consumer of @deepseek-ai/dsh@0.1.0-rc.6 (npm private package):
- Types/runtime: peers are
@deepseek-ai/cordis: ^4.0.1+@deepseek-ai/dsh-tools: >=0.0.1-rc.1 <0.2.0+@deepseek-ai/dsh-invariants: >=0.0.1-rc.1 <0.2.0; no longer depends on the unscopedcordis - Standalone build:
npm install(devDependencies are self-contained: typescript/vitest/@types/node) →npm run typecheck→npm test→npm run build→npm pack - Consumption verification: tarball installed into a DSH 0.1.0-rc.6 (npm) consumer →
dsh --profile compat --dump-configshows this plugin's row → tool registration and execution actually pass - Launch method:
npx -p @deepseek-ai/dsh@0.1.0-rc.6 dsh web(lib production mode; do notinstall -gglobally)
Version Adaptation
- Adapted DSH: DSH 0.1.0-rc.6 (npm) (profile/bundle plugin system)
- bundle declaration:
dsh.bundleinpackage.json(patch points tocordis.patch.yml) +exportsexports - patch format:
cordis.patch.ymluses a- insert:list (the patch is id-targeted; bare- id:entries reportentry not found) - files: the published tarball includes
lib/,src/,cordis.patch.yml
Installation
Profile Bundle (Recommended)
Install this plugin into a profile as a standalone bundle (DSH 0.1.0-rc.6 (npm)). This repository lives under the omdsh-dev organization and is publicly accessible:
# Interactive (web) profile —— install from the GitHub repository
dsh plugin --profile web add github:omdsh-dev/dsh-tool-time
# One-off task (headless) profile —— dsh run uses headless by default
dsh plugin --profile headless add github:omdsh-dev/dsh-tool-time
Or install from the tarball produced by npm pack:
npm pack # produces dsh-tool-time-<version>.tgz
# Interactive (web) profile
dsh plugin --profile web add ./dsh-tool-time-<version>.tgz
# One-off task (headless) profile
dsh plugin --profile headless add ./dsh-tool-time-<version>.tgz
The bundled dsh.bundle.patch (pointing to cordis.patch.yml) automatically adds the plugin to the profile's layer stack after installation; the plugin's cordis.patch.yml inserts the tool-time entry via - insert:. The plugin's missing peer dependencies (@deepseek-ai/cordis, @deepseek-ai/dsh-tools) are provided by the profile's healed profiles/node_modules fallback installation.
⚠️ web and headless are different profiles: installing into web does not automatically cover headless;
dsh runuses the headless profile by default. Use forward slashes for Windows paths (C:/...).
Verify Installation
dsh --profile web --dump-config | grep tool-time
Runtime Verification
dsh run "使用 time 工具获取当前 UTC 时间"
Manual Installation and Legacy Compatibility (legacy monorepo scenario)
The monorepo way is only for legacy scenarios: old snapshots that do not support Profile Bundle, or plugin development/debugging environments:
- Place into the monorepo:
cp -r time ~/.dsh/source/master/packages/tools/time(development debugging) - Add
"@deepseek-ai/dsh-tool-time": "workspace:^"toapps/cli/package.json; add{ "path": "./packages/tools/time" }to thetsconfig.host.jsonreferences pnpm install && pnpm run build- Insert the plugin in the profile's user-layer patch (
~/.dsh/profiles/<name>/cordis.patch.yml):
- insert:
- id: tool-time
name: '@deepseek-ai/dsh-tool-time'
- Verify:
dsh --profile <name> --dump-config | grep tool-time
Note: the patch is id-targeted (DSH 0.1.0-rc.6 (npm)) — a bare
- id:entry reportsentry "xxx" not found; you must wrap it in a- insert:list.
Known Limitations
- Distribution chain: peer dependencies (e.g.
@deepseek-ai/dsh-tools) are npm private packages; standalone installation does not depend on the monorepo workspace (monorepo is legacy only) addis UTC calendar arithmetic, not local wall-clock semantics (local wall-clock requires v2)- Natural-language dates ("next week") and calendar month/year diffs are not supported (v2)
- Timezone data comes from Node's built-in ICU: "cross-platform consistency" means "consistency under the same Node/ICU data"
- Input year range is 1000–9999 (
0000–0999rejected: JavaScriptDate.UTChas a historical 1900+ mapping behavior for those years) - Precision contract:
iso/unixpreserve milliseconds;local/formattedare second-precision display fields - Offset precision down to minutes: v1 only supports modern timezones; second-level offsets of historical timezones (<1900) are truncated
Tests
pnpm test
63 cases covering functionality/errors/attack payloads (fake clock, known timezone vectors, end-of-month clamping, strict ISO boundaries, year ranges, offsets crossing year boundaries, extreme amounts, prototype-chain timezone names, etc.). See the locally maintained design doc for the full list.
License
MIT
Links
More in this category
liustack/modlens★ 1199
Vision bridge for text-only models: paste an image, get structured JSON evidence (OCR, layout, semantics).
Anionex/dsh-vision-toolkit★ 308
Vision tasks for text-only models: intent-aware image Q&A, long-screenshot OCR, UI reproduction, grounding, and pixel diff.
zhaoolee/notes★ 138
Export DSH conversations as Smartisan Notes-style PNGs, or create and update Markdown notes in a configured account-scoped workspace.
liustack/modsearch★ 85
Web search bridge for text-only agents: ask the web or X, get structured JSON evidence (search, fetch, citations).
Lum1104/dsh-browser★ 80
Chrome sidebar extension that lets DSH operate your browser directly, no vision capabilities required.
taxueseek/argo★ 69
Search built for agents: multilingual coverage across web, academic, code, shopping, finance, news, and encyclopedias.