Skill: abdelkrim/openclaw-skill-m365-task-manager-by-altf1be (version 0.1.2)

--- name: openclaw-skill-m365-task-manager-by-altf1be description: "Manage lightweight Microsoft 365 task workflows with Microsoft To Do and Planner. Use when a user needs to quickly create, assign, track, and follow up operational tasks in M365 with clear owners, due dates, status, and daily reminders." homepage: https://github.com/ALT-F1-OpenClaw/openclaw-skill-m365-task-manager metadata: {"openclaw": {"emoji": "✅", "requires": {"env": ["M365_TENANT_ID", "M365_CLIENT_ID"]}, "primaryEnv": "M365_TENANT_ID"}} --- # M365 Task Manager Use this skill to perform real Microsoft Graph CRUD operations for Microsoft To Do tasks. ## Setup 1. Create an Entra app registration for delegated sign-in. 2. Add Microsoft Graph delegated permissions: - `Tasks.ReadWrite` - `User.Read` - `offline_access` 3. Configure environment variables: ```bash M365_TENANT_ID=your-tenant-id-or-common M365_CLIENT_ID=your-public-client-app-id # optional M365_TOKEN_CACHE_PATH=/home/user/.cache/openclaw/m365-task-manager-token.json ``` 4. Install dependencies at repo root: ```bash npm install ``` On first run, the script uses Device Code login and caches tokens for reuse. ## Commands ```bash # profile connection node skills/m365-task-manager/scripts/m365-todo.mjs info # list Microsoft To Do lists node skills/m365-task-manager/scripts/m365-todo.mjs lists # list tasks node skills/m365-task-manager/scripts/m365-todo.mjs tasks:list --list-name "Tasks" # create task node skills/m365-task-manager/scripts/m365-todo.mjs tasks:create --list-name "Tasks" --title [REDACTED_SECRET] --due 2026-03-01 # update task node skills/m365-task-manager/scripts/m365-todo.mjs tasks:update --list-name "Tasks" --task-id <TASK_ID> --status inProgress # delete task node skills/m365-task-manager/scripts/m365-todo.mjs tasks:delete --list-name "Tasks" --task-id <TASK_ID> ``` ## Operating standard - Task title pattern: `YYYY-MM-DD-short-action-owner` - Required fields: title, owner, due date, status - Status values: `Open`, `In Progress`, `Blocked`, `Done` ## References - `references/playbook.md` for operating guidance. ## Scripts - `scripts/m365-todo.mjs` for Graph CRUD on Microsoft To Do. - `scripts/format-task-name.sh` for deterministic task naming. ## Author Abdelkrim BOUJRAF - ALT-F1 SRL - https://www.alt-f1.be ## License MIT

path: references/playbook.md
content: # M365 Task Manager Playbook

## Technical execution model

- API: Microsoft Graph
- Auth: delegated Device Code flow
- Token cache: local file for unattended reuse after first login

## CLI command map

- `info` - verify authenticated user and scopes
- `lists` - list Microsoft To Do lists
- `tasks:list` - read tasks from list
- `tasks:create` - create task
- `tasks:update` - patch task fields
- `tasks:delete` - remove task

## Functional guidance

### Use Microsoft To Do when
- Single user personal execution
- Fast operational capture

### Use Planner when
- Team board visibility is required
- Multi-owner orchestration is required

## Status lifecycle

- Open
- In Progress
- Blocked
- Done

## Naming convention

Pattern:
- `YYYY-MM-DD-short-action-owner`

Examples:
- `2026-02-24-burn-2-dvd-send-robert`
- `2026-02-24-review-m365-license-assignment`


path: scripts/format-task-name.sh
content: #!/usr/bin/env bash
set -euo pipefail

if [ "$#" -lt 2 ]; then
  echo "Usage: $0 <date> <short-action>"
  exit 1
fi

date="$1"
action="$2"
slug=$(echo "$action" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g')
echo "${date}-${slug}"


path: scripts/m365-todo.mjs
content: #!/usr/bin/env node

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import { PublicClientApplication } from '@azure/msal-node';

const GRAPH_BASE = 'https://graph.microsoft.com/v1.0';
const SCOPES = ['User.Read', 'Tasks.ReadWrite', 'offline_access'];

function env(name, required = true, fallback = undefined) {
  const v = process.env[name] ?? fallback;
  if (required && !v) throw new Error(`Missing required env var: ${name}`);
  return v;
}

function defaultCachePath() {
  return path.join(os.homedir(), '.cache', 'openclaw', 'm365-task-manager-token.json');
}

function ensureDir(filePath) {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
}

function parseArgs(argv) {
  const args = { _: [] };
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (a.startsWith('--')) {
      const [k, inline] = a.split('=');
      const key = k.slice(2);
      if (inline !== undefined) args[key] = inline;
      else if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) args[key] = argv[++i];
      else args[key] = true;
    } else {
      args._.push(a);
    }
  }
  return args;
}

function help() {
  console.log(`m365-todo - Microsoft Graph To Do CRUD\n
Usage:
  m365-todo info
  m365-todo lists
  m365-todo tasks:list --list-id <id>
  m365-todo tasks:list --list-name "Tasks"
  m365-todo tasks:create --list-id <id> --title "Task title" [--body "..."] [--due 2026-02-28]
  m365-todo tasks:update --list-id <id> --task-id <id> [--title "..."] [--body "..."] [--status notStarted|inProgress|completed|waitingOnOthers|deferred] [--due YYYY-MM-DD]
  m365-todo tasks:delete --list-id <id> --task-id <id>

Required env:
  M365_TENANT_ID
  M365_CLIENT_ID
Optional env:
  M365_TOKEN_CACHE_PATH
`);
}

async function createPca() {
  const tenantId = env('M365_TENANT_ID');
  const clientId = env('M365_CLIENT_ID');
  const cachePath = env('M365_TOKEN_CACHE_PATH', false, defaultCachePath());

  const pca = new PublicClientApplication({
    auth: {
      clientId,
      authority: `https://login.microsoftonline.com/${tenantId}`,
    },
  });

  ensureDir(cachePath);
  if (fs.existsSync(cachePath)) {
    try {
      const raw = fs.readFileSync(cachePath, 'utf8');
      pca.getTokenCache().deserialize(raw);
    } catch {
      // ignore corrupted cache, device login will refresh
    }
  }

  return { pca, cachePath };
}

async function saveCache(pca, cachePath) {
  const serialized = await pca.getTokenCache().serialize();
  fs.writeFileSync(cachePath, serialized, 'utf8');
}

async function getAccessToken() {
  const { pca, cachePath } = await createPca();
  const accounts = await pca.getTokenCache().getAllAccounts();

  if (accounts.length > 0) {
    try {
      const silent = await pca.acquireTokenSilent({
        account: accounts[0],
        scopes: SCOPES,
      });
      if (silent?.accessToken) {
        await saveCache(pca, cachePath);
        return silent.accessToken;
      }
    } catch {
      // fallback to device code
    }
  }

  const device = await pca.acquireTokenByDeviceCode({
    scopes: SCOPES,
    deviceCodeCallback: (resp) => {
      console.error(resp.message);
    },
  });

  if (!device?.accessToken) throw new Error('Failed to acquire access token');
  await saveCache(pca, cachePath);
  return device.accessToken;
}

async function graph(method, endpoint, token, body) {
  const res = await fetch(`${GRAPH_BASE}${endpoint}`, {
    method,
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: body ? JSON.stringify(body) : undefined,
  });

  if (!res.ok) {
    let detail = '';
    try {
      const j = await res.json();
      detail = j?.error?.message || JSON.stringify(j);
    } catch {
      detail = await res.text();
    }
    throw new Error(`Graph ${method} ${endpoint} failed (${res.status}): ${detail}`);
  }

  if (res.status === 204) return null;
  return res.json();
}

function must(args, key) {
  const v = args[key];
  if (!v) throw new Error(`Missing --${key}`);
  return v;
}

function parseDueDate(d) {
  if (!d) return undefined;
  if (!/^\d{4}-\d{2}-\d{2}$/.test(d)) throw new Error('Invalid --due format. Use YYYY-MM-DD');
  return { dateTime: `${d}T17:00:00`, timeZone: 'Europe/Brussels' };
}

async function resolveListId(args, token) {
  if (args['list-id']) return args['list-id'];
  if (!args['list-name']) throw new Error('Provide --list-id or --list-name');

  const data = await graph('GET', '/me/todo/lists', token);
  const found = data.value.find((x) => x.displayName.toLowerCase() === String(args['list-name']).toLowerCase());
  if (!found) throw new Error(`List not found: ${args['list-name']}`);
  return found.id;
}

async function cmdInfo(token) {
  const me = await graph('GET', '/me?$select=id,displayName,userPrincipalName', token);
  console.log(JSON.stringify({
    connected: true,
    user: me,
    scopes: SCOPES,
  }, null, 2));
}

async function cmdLists(token) {
  const data = await graph('GET', '/me/todo/lists', token);
  console.log(JSON.stringify(data.value, null, 2));
}

async function cmdTasksList(args, token) {
  const listId = await resolveListId(args, token);
  const data = await graph('GET', `/me/todo/lists/${listId}/tasks?$top=100`, token);
  console.log(JSON.stringify(data.value, null, 2));
}

async function cmdTasksCreate(args, token) {
  const listId = await resolveListId(args, token);
  const title = must(args, 'title');
  const body = {
    title,
  };
  if (args.body) body.body = { content: String(args.body), contentType: 'text' };
  const due = parseDueDate(args.due);
  if (due) body.dueDateTime = due;

  const created = await graph('POST', `/me/todo/lists/${listId}/tasks`, token, body);
  console.log(JSON.stringify(created, null, 2));
}

async function cmdTasksUpdate(args, token) {
  const listId = await resolveListId(args, token);
  const taskId = must(args, 'task-id');
  const patch = {};

  if (args.title) patch.title = String(args.title);
  if (args.body) patch.body = { content: String(args.body), contentType: 'text' };
  if (args.status) patch.status = String(args.status);
  if (args.due) patch.dueDateTime = parseDueDate(args.due);

  if (Object.keys(patch).length === 0) throw new Error('No fields to update. Use --title/--body/--status/--due');

  const updated = await graph('PATCH', `/me/todo/lists/${listId}/tasks/${taskId}`, token, patch);
  console.log(JSON.stringify(updated, null, 2));
}

async function cmdTasksDelete(args, token) {
  const listId = await resolveListId(args, token);
  const taskId = must(args, 'task-id');
  await graph('DELETE', `/me/todo/lists/${listId}/tasks/${taskId}`, token);
  console.log(JSON.stringify({ deleted: true, listId, taskId }, null, 2));
}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  const command = args._[0];

  if (!command || command === '--help' || command === 'help') {
    help();
    process.exit(0);
  }

  const token = await getAccessToken();

  switch (command) {
    case 'info': return cmdInfo(token);
    case 'lists': return cmdLists(token);
    case 'tasks:list': return cmdTasksList(args, token);
    case 'tasks:create': return cmdTasksCreate(args, token);
    case 'tasks:update': return cmdTasksUpdate(args, token);
    case 'tasks:delete': return cmdTasksDelete(args, token);
    default:
      help();
      throw new Error(`Unknown command: ${command}`);
  }
}

main().catch((err) => {
  console.error(`ERROR: ${err.message}`);
  process.exit(1);
});


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)